fixed a few logic bugs with quarterly data, now accurately puts bill proportions into relevant quarters and estimates future bills based on quarterly data - all works so far

This commit is contained in:
2025-08-25 18:46:24 +10:00
parent 65fc68e0bf
commit 89d58e4cd3

127
bills.py
View File

@@ -5,8 +5,8 @@ from datetime import date, timedelta
################################################################################
# this finds start and end dates of a quarter for a given date
################################################################################
def quarter_bounds(d):
q = (d.month-1)//3
start = date(d.year, 3*q+1, 1)
@@ -18,10 +18,18 @@ def quarter_bounds(d):
end = next_start - timedelta(days=1)
return start, end
# this needs tweaking to be used to add to our total - but is close
def allocate_by_quarter( bill_info, bill_type, yr, prev_bill_date, curr_bill_date, amount, include_start=False, include_end=True):
start = prev_bill_date if include_start else prev_bill_date + timedelta(days=1)
end = curr_bill_date if include_end else curr_bill_date - timedelta(days=1)
################################################################################
# takes a bill and its previous bill, works out days between and adds cost / day
# to each quarter the bill covers from prev. to now. Usually means it splits
# one bill in a previous and this qtr (or just puts it all into the current qtr)
################################################################################
def allocate_by_quarter( bill_info, bill_type, yr, prev_bill, bill):
start = date( int(prev_bill['bill_date'][:4]), int(prev_bill['bill_date'][5:7]), int(prev_bill['bill_date'][8:]))
end = date( int(bill['bill_date'][:4]), int(bill['bill_date'][5:7]), int(bill['bill_date'][8:]))
time_difference = end - start
days = time_difference.days
cost_per_day = bill['amount']/days
if end < start:
return {}
if not 'qtr' in bill_info[bill_type]:
@@ -38,24 +46,26 @@ def allocate_by_quarter( bill_info, bill_type, yr, prev_bill_date, curr_bill_dat
if overlap_end >= overlap_start:
days = (overlap_end - overlap_start).days + 1
q = (q_start.month-1)//3 + 1
# NEED LOGIC TO INIT IF ITS NOT HERE YET
# initialise arrays if needed
if q_start.year not in bill_info[bill_type]['qtr']:
bill_info[bill_type]['qtr'][q_start.year] = {}
for i in range(1,5):
bill_info[bill_type]['qtr'][q_start.year][i]=0
# print( f"^^^^^^ adding {days*amount} into q={q}, yr={q_start.year}, with pbd={prev_bill_date}, cbd={curr_bill_date}, cba={amount}" )
bill_info[bill_type]['qtr'][q_start.year][q] += days*amount
bill_info[bill_type]['qtr'][q_start.year][q] += days*cost_per_day
# next quarter
cur = q_end + timedelta(days=1)
return
################################################################################
# give a bill dat in format YYYY-MM-DD, return quarter (1-4)
# given a bill date in format YYYY-MM-DD, return quarter (1-4)
################################################################################
def qtr(d):
m = int(d[5:7])
return ( (m-1)//3 + 1 )
################################################################################
# find the bill just after the date given
################################################################################
def find_next_bill( bill_type, bill_info, bill_date ):
wanted_year = int(bill_date[:4])
wanted_mm = int(bill_date[5:7])
@@ -76,6 +86,7 @@ def find_next_bill( bill_type, bill_info, bill_date ):
return None
# find the bill just before the date given
def find_previous_bill( bill_type, bill_info, bill_date ):
wanted_year = int(bill_date[:4])
wanted_mm = int(bill_date[5:7])
@@ -109,6 +120,9 @@ def find_previous_bill( bill_type, bill_info, bill_date ):
return None
# quick wrapper to add a new estimated bill - new estimates have the flag in
# the DB set, but also we update bill_info to reflect the new bill so future
# growth can build of this esimate too - e.g 2030 can use 2029, etc
def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
# add to DB
new_bill( bill_type, amt, new_date, 1 )
@@ -121,7 +135,18 @@ def new_estimated_bill( bill_info, yr, bill_type, amt, new_date ):
bill['amount']=amt
bill['estimated']=1
# need this for find_previous_bill to work but only need the above 3 fields
bill_info[bill_type]['year'][yr].append(bill)
bill_info[bill_type]['year'][yr].insert(0,bill)
if bill_info[bill_type]['num_ann_bills'] == 4:
q = qtr( new_date )
if yr not in bill_info[bill_type]['qtr']:
bill_info[bill_type]['qtr'][yr]={}
pb = find_previous_bill( bill_type, bill_info, new_date )
if pb['estimated'] == 0:
print( f" FIXFIXFIX - have a prev real bill={pb['bill_date']} & this is first est - likely need to better apportion this bill into the quarters" )
allocate_by_quarter( bill_info, bill_type, yr, pb, bill )
bill_info[bill_type]['qtr'][yr][q]=amt
return
@@ -135,32 +160,33 @@ def add_missing_annual_bill_in_yr( bill_type, bill_info, yr ):
for i in range( bill_info[bill_type]['last_bill_year'], yr ):
amt += amt * bill_info[bill_type]['growth']/100
# last param is estimated (and this is an estimate for a future bill / not real)
new_estimated_bill( bill_info, yr, bill_type, amt, f'{yr}-{mm_dd}' )
return
# missing quarterly bill, find date based on MM-DD and ??? - can have missing bilsl in first year
# add growth (based on drop-down) for each future year
def add_missing_quarter_bills_in_yr( bill_type, bill_info, yr ):
# okay we have data for this year but some missing (wouldnt be here otherwise)
# and data from previous year... lets fill in gaps
if yr in bill_info[bill_type]['year'] and yr-1 in bill_info[bill_type]['year']:
# okay we have data for last year but some missing (in this year), lets fill in gaps
# could be called if only have data for q2 - q4 in first year and we dont have a previous years q1 data so don't try
if 'qtr' in bill_info[bill_type] and yr-1 in bill_info[bill_type]['qtr']:
# if we do have data in this year, we have q1-q3 only, and want missing qtrs set range appropriately...
if yr in bill_info[bill_type]['qtr']:
# per if above, ONLY get here if we have first few bills of {yr}, cannot be last few
have_q = qtr( bill_info[bill_type]['year'][yr][0]['bill_date'] )
for q in range(have_q+1,5):
# use 5-q, as bills are in descending order in bill_info, e.g. q4 is 1st,
bill=bill_info[bill_type]['year'][yr-1][4-q]
mm_dd= bill['bill_date'][5:]
amt = bill['amount']*(1+bill_info[bill_type]['growth']/100)
new_date = f'{yr}-{mm_dd}'
r=range(have_q+1,5)
else:
r=range(1,5)
for q in r:
# amt is total of last year's qtr bill proportion
amt = bill_info[bill_type]['qtr'][yr-1][q]*(1+bill_info[bill_type]['growth']/100)
# just make new bills first of last month of a qtr (good as any date for GAS, they move anyway)
new_date = f'{yr}-{q*3:02d}-01'
# SANITY CHECK: we might be adding a bill estimate we already have (due to stupid gas bills /qtrly code)
if yr in bill_info[bill_type]['year']:
for b in bill_info[bill_type]['year'][yr]:
if b['bill_date'] == new_date:
return
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
# for now only add full new years based on last year with ann_growth (seasonal)
if yr not in bill_info[bill_type]['year'] and yr-1 in bill_info[bill_type]['year']:
for bill in bill_info[bill_type]['year'][yr-1]:
mm_dd= bill['bill_date'][5:]
amt = bill['amount']*(1+bill_info[bill_type]['growth']/100)
new_estimated_bill( bill_info, yr, bill_type, amt, f'{yr}-{mm_dd}' )
return
# missing monthly bills, find date based on DD and put in each missing month
@@ -208,7 +234,6 @@ def add_missing_monthly_bills_in_yr( bill_type, bill_info, yr ):
if i == int(lb_mm):
amt += amt * bill_info[bill_type]['growth']/100
bill_info[bill_type]['last_bill_amount']=amt
# last param is estimated (and this is an estimate for a future bill / not real)
new_estimated_bill( bill_info, yr, bill_type, amt, new_date )
return
@@ -227,10 +252,12 @@ def get_growth_value( bt, bill_type ):
return el['ann_growth_max']
################################################################################
# go through the bill data from the DB, put it into more friendly formats, then
# work out and then add missing bill data (might be b/c we have monthly bills,
# and I didn't want to input 12 of them at the same price), and it always
# occurs for future bills
################################################################################
def process_bill_data(bd, bt, bf):
# this maps a bill id to a freq id (e.g. bill #34 - has a frequency of #2 (which might be quarterly)
bt_id_freq = {row["id"]: row["freq"] for row in bt}
@@ -281,10 +308,12 @@ def process_bill_data(bd, bt, bf):
yr_min=int(bill_info[bill_type]['first_bill']['bill_date'][:4])
yr_max=int(bill_info[bill_type]['last_bill']['bill_date'][:4])
ProportionQtrlyData( bill_type, bill_info )
# go from first_bill year until reach end year
for yr in range( yr_min, END_YEAR+1 ):
# we have all the bills needed for yr
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) == bill_info[bill_type]['num_ann_bills']:
# we have all the bills needed for yr - but dont be cute with qtrly, gas bills suck can have missing with 4 bills
if yr in bill_info[bill_type]['year'] and len(bill_info[bill_type]['year'][yr]) == bill_info[bill_type]['num_ann_bills'] and bill_info[bill_type]['num_ann_bills'] !=4:
continue
add_missing_bills_for_yr( bill_type, bill_info, yr )
derive_ann_growth( bill_type, bill_info )
@@ -302,10 +331,17 @@ def add_missing_bills_for_yr( bill_type, bill_info, yr ):
add_missing_monthly_bills_in_yr( bill_type, bill_info, yr )
return
def derive_ann_growth( bill_type, bill_info ):
# just do up to now so we stop earlier than looking at other estimated (just an optimisation)
################################################################################
# Takes qtrly bills and start from 2nd year of bills (so we can estimate growth)
# and go through each bill allocating hte proportion of each bill to each
# relevant quarter - to build more accurate totals. Would be mostly marginal
# accept when Gas qtrly bills have 6 per year, and we need to guess say qtr4 in
# the future, we can't easily find corresponding bill form previous year, so
# this allows us to aggregate per quarter and use matching quarter
################################################################################
def ProportionQtrlyData( bill_type, bill_info ):
# just do up to now for the moment so that add_missing_bills later will have qtr data to use
now_yr = datetime.date.today().year
# FIX UP CRAPPY QUARTERLY BILLING PROPORTIONS (only useful as some gas bills are 6 / year!)
if bill_info[bill_type]['num_ann_bills']==4:
for yr in range( bill_info[bill_type]['first_bill_year'], now_yr+1):
@@ -313,16 +349,17 @@ def derive_ann_growth( bill_type, bill_info ):
pb = find_previous_bill( bill_type, bill_info, b['bill_date'] )
if not pb:
continue
allocate_by_quarter( bill_info, bill_type, yr, pb, b )
return
pb_d=pb['bill_date']
b_d=b['bill_date']
date1 = date( int(pb_d[:4]), int(pb_d[5:7]), int(pb_d[8:]))
date2 = date( int(b_d[:4]), int(b_d[5:7]), int(b_d[8:]))
time_difference = date2 - date1
days = time_difference.days
cost_per_day = b['amount']/days
allocate_by_quarter( bill_info, bill_type, yr, date1, date2, cost_per_day )
################################################################################
# function to work out totals per year, and then calcuates annual growth in
# terms of min/avg/max - uses qtr data for qtrly bills, or just normal totals
# for other bill types
################################################################################
def derive_ann_growth( bill_type, bill_info ):
# just do up to now so we stop earlier than looking at other estimated (just an optimisation)
now_yr = datetime.date.today().year
total={}
for yr in range( bill_info[bill_type]['first_bill_year'], now_yr+1):
@@ -342,8 +379,8 @@ def derive_ann_growth( bill_type, bill_info ):
total[yr] = 0
for b in bill_info[bill_type]['year'][yr]:
total[yr] += b['amount']
#crazily we have more bills in this year than expected, so work out qtrly costs
# crazily we can have more bills in this year than expected, so work out qtrly costs, and patch that back into total array
for yr in range( bill_info[bill_type]['first_bill_year'], now_yr+1):
if 'qtr' in bill_info[bill_type] and yr in bill_info[bill_type]['qtr']:
tot=0
@@ -372,7 +409,7 @@ def derive_ann_growth( bill_type, bill_info ):
if growth > max_growth:
max_growth = growth
if count:
print( f"Before sanity check, min={min_growth}, avg={avg_growth/count}, max_growth={max_growth}" )
## print( f"Before sanity check, min={min_growth}, avg={avg_growth/count}, max_growth={max_growth}" )
# HACK FOR SANITY SAKE NOW - bills wont decrease normally, and 10% is unlikely for sustained growth
if min_growth< 0: min_growth=0
if avg_growth< 0 or avg_growth > 10: avg_growth = 3*count