Calculate highscores based on sales data

Posted on

Problem

The background is my question is SO How to create a list of totals for durations?
where I found a recursive solution that I want to review. I could not test it for higher levels than 1 but to the best of my knowledge it should work.

What I’m trying to do is calculate a kind of “highscore”, i.e. a level based on sales during the two historical months where sales where the most. So I find the oldest purchase, use the helper methods to calculate the bonus for that period and then check to see whether that period is the entitling to a level upgrade. It’s basically going through all the historical orders.

Is my recursion written correctly?

def level(self):
    startdate = model.Order.all().filter('status =', 'PAID'
            ).filter('distributor_id =',
                     self._key.id()).get().created.date()
    last_day_nextmonth = startdate + relativedelta(months=2) 
        - timedelta(days=1)
    total = self.personal_silver(startdate, last_day_nextmonth) + self.non_manager_silver(startdate, last_day_nextmonth)
    if total >= 125:
        level = 5
    elif total >= 75:
        level = 4
    elif total >= 25:
        level = 3
    elif total >= 2:
        level = 2
    else:
        level = 1
    return self.levelHelp(level, last_day_nextmonth + timedelta(days=1))

def levelHelp(self, level, startdate):

    last_day_nextmonth = startdate + relativedelta(months=2) 
        - timedelta(days=1)
    total = self.personal_silver(startdate, last_day_nextmonth) + self.non_manager_silver(startdate, last_day_nextmonth)
    if total >= 125:
        newlevel = 5
    elif total >= 75:
        newlevel = 4
    elif total >= 25:
        newlevel = 3
    elif total >= 2:
        newlevel = 2
    else:
        newlevel = 1

    maxlevel = level if level > newlevel else newlevel

    nextstart = last_day_nextmonth + timedelta(days=1)
    now = datetime.now().date()
    if nextstart > now: #next start in is the future
        return maxlevel
    else: return self.levelHelp(maxlevel, nextstart)

The helper functions are

def non_manager_silver(self, startdate, enddate):
    silver = 0
    timeline = date(startdate.year, startdate.month, 1)
    downline = User.query(User.sponsor == self._key).fetch()
    distributor = self
    while distributor.has_downline():
        downline = User.query(User.sponsor
                              == distributor.key).fetch()
        for person in downline:
            orders = model.Order.all().filter('distributor_id =',
                    person.key.id()).filter('created >',
                    startdate).filter('created <',
                    enddate).filter('status =', 'PAID'
                    ).fetch(999999)
            for order in orders:
                for (idx, item) in enumerate(order.items):
                    purchase = model.Item.get_by_id(long(item.id()))
                    amount = int(order.amounts[idx])
                    silver = silver + amount * purchase.silver 
                        / 1000.000
            distributor = person
    return silver

def personal_silver(self, startdate, enddate):
    p_silver = 0
    timeline = date(startdate.year, startdate.month, 1)  # first day of month
    orders = model.Order.all().filter('distributor_id =',
            self._key.id()).filter('created >',
            timeline).filter('created <', enddate).filter('status ='
            , 'PAID').fetch(999999)
    for order in orders:
        for (idx, item) in enumerate(order.items):
            purchase = model.Item.get_by_id(long(item.id()))
            amount = int(order.amounts[idx])
            p_silver = p_silver + amount * purchase.silver 
                / 1000.000  # remember amounts
    return p_silver

Solution

def level(self):
    startdate = model.Order.all().filter('status =', 'PAID'
            ).filter('distributor_id =',
                     self._key.id()).get().created.date()
    last_day_nextmonth = startdate + relativedelta(months=2) 
        - timedelta(days=1)

Continuing lines with is generally frowned upon. Either split it onto multiple lines or wrap the expression with parenthesis

    total = self.personal_silver(startdate, last_day_nextmonth) + self.non_manager_silver(startdate, last_day_nextmonth)

I think writing a self.silver(start_date, end_date) function would make sense to get the sum of those two.

    if total >= 125:
        level = 5
    elif total >= 75:
        level = 4
    elif total >= 25:
        level = 3
    elif total >= 2:
        level = 2
    else:
        level = 1

I’d store all of these levels in a list like [0, 2, 25, 75, 125]. Then I’d use functions from bisect to figure out which level I was at.

    return self.levelHelp(level, last_day_nextmonth + timedelta(days=1))


def levelHelp(self, level, startdate):

    last_day_nextmonth = startdate + relativedelta(months=2) 
        - timedelta(days=1)
    total = self.personal_silver(startdate, last_day_nextmonth) + self.non_manager_silver(startdate, last_day_nextmonth)
    if total >= 125:
        newlevel = 5
    elif total >= 75:
        newlevel = 4
    elif total >= 25:
        newlevel = 3
    elif total >= 2:
        newlevel = 2
    else:
        newlevel = 1

Deja Vu! You’ve got the exact same code again, at the very least, move the common code into another function.

    maxlevel = level if level > newlevel else newlevel

Use maxlevel = max(level, newlevel)

    nextstart = last_day_nextmonth + timedelta(days=1)
    now = datetime.now().date()
    if nextstart > now: #next start in is the future
        return maxlevel
    else: return self.levelHelp(maxlevel, nextstart)

You shouldn’t really be using recursion here. This is very naturally a while loop. I think if you rewrite this as a while loop, it’ll be clearer and you’ll be able to combine these two functions.

def non_manager_silver(self, startdate, enddate):
    silver = 0
    timeline = date(startdate.year, startdate.month, 1)

I’m not clear why you back this up to the start of the month rather then using the startdate.

    downline = User.query(User.sponsor == self._key).fetch()

This has no effect, because you throw it away as soon as you enter the loop

    distributor = self
    while distributor.has_downline():
        downline = User.query(User.sponsor
                              == distributor.key).fetch()

        for person in downline:
            orders = model.Order.all().filter('distributor_id =',
                    person.key.id()).filter('created >',
                    startdate).filter('created <',
                    enddate).filter('status =', 'PAID'
                    ).fetch(999999)

Why 999999?

            for order in orders:
                for (idx, item) in enumerate(order.items):

No need for those parens.

                    purchase = model.Item.get_by_id(long(item.id()))
                    amount = int(order.amounts[idx])

Using int and long seems a little odd. Are you converting from strings?

                    silver = silver + amount * purchase.silver 
                        / 1000.000

Use silver += amount * purchase.silver / 1000.000

            distributor = person

See, here is where you should be using recursion. I assume that you are trying to go through the downlines of all the downlines. However, as your code is written, you’ll only go through the downlines of the last member of the downline.

    return silver

def personal_silver(self, startdate, enddate):
    p_silver = 0
    timeline = date(startdate.year, startdate.month, 1)  # first day of month
    orders = model.Order.all().filter('distributor_id =',
            self._key.id()).filter('created >',
            timeline).filter('created <', enddate).filter('status ='
            , 'PAID').fetch(999999)
    for order in orders:
        for (idx, item) in enumerate(order.items):
            purchase = model.Item.get_by_id(long(item.id()))
            amount = int(order.amounts[idx])
            p_silver = p_silver + amount * purchase.silver 
                / 1000.000  # remember amounts

    return p_silver

This function is pretty much exactly the same as the previous function. You should be able to move common parts into a function.

I’d approach this whole thing quite differently.

def find_downline(distributor):
    """
    Given a distributor, collect all the members of his downline into a list
    """
    downline = [distributor]
    immediate_downline = User.query(User.sponsor == distributor.key)
    for member in immediate_downline:
        downline.extend( find_downline(member) )
    return downline

def find_orders(distributor):
    return model.Order.all().filter('distributor_id =',
            self._key.id()).filter('status ='
            , 'PAID').fetch()

def silver_in_order(order):
    total = 0
    for item, amount in zip(order.items, order.amounts):
        purchase = model.Item.get_by_id(long(item.id()))
        total += amount * purchase.silver
    return total

def silver_bimonth(distributor):
    bimonths = collections.defaultdict(int)
    for member in find_downline(distributor):
         for order in find_orders(member):
             date = order.created.date
             bimonth_index = (date.year * 12 + date.month) / 2
             bimonths[bimonth_index] += silver_in_order(order)
    return = max(bimonths.value)

def level(distributor):
    silver = silver_bimonth(distributor)
    return bisect.bisect_left(LEVELS, silver) + 1

Zero testing. Probably doesn’t work. But it should give an idea of an alternate approach. Basically, I figure that since you are going to fetch all the orders anyways, it makes more sense to fetch them all figure which bimonthly period they belong to, and figure out the maximum that way.

Leave a Reply

Your email address will not be published. Required fields are marked *