diff --git a/bpfin/financials/financial_lib.py b/bpfin/financials/financial_lib.py index dcf1358f0a3c44ae22126431c0aeda00689ab205..9245521810be96ba6a6cdd43ac3adf9b113228aa 100644 --- a/bpfin/financials/financial_lib.py +++ b/bpfin/financials/financial_lib.py @@ -1,13 +1,16 @@ # import datetime # import calendar -# from bpfin.lib import other as lib -import bpfin.utilbills.bill_lib as bl -from bpfin.tests.testdata import sample_data as db +import copy +from bpfin.lib import other as lib +from bpfin.utilbills.bill_lib import form_bill_year +from numpy import mean +# from bpfin.tests.testdata import sample_data as db +# import bpfin.utilbills.bill_lib as bl # import pprint def income_statement_single_year(year, income_input, bill_overview): - """calculate income statement for one single year, with inputs from UI and energy bill overview + """calculate income statement for one single year, with some inputs and energy bill overview Args: year (int): the year of this income statement @@ -34,12 +37,6 @@ def income_statement_single_year(year, income_input, bill_overview): net_non_energy_opex = other_utility + non_utility_expense total_opex = energy_opex + net_non_energy_opex noi = revenue - total_opex # net operating income - # energy_debt_service = - # other_debt_service = - # total_debt_service = - # bill_saving = - # energy_DSCR = - # total_DSCR = income_statement = { 'year': year, @@ -71,8 +68,8 @@ def organize_bill_overview(bill_overview, analysis_date): Description: analysis_date: {'proforma_start': datetime.date, 'proforma_duration': int} """ - proforma_year = bl.form_bill_year(analysis_date['proforma_start'], - analysis_date['proforma_duration']) + proforma_year = form_bill_year(analysis_date['proforma_start'], + analysis_date['proforma_duration']) bill_dict = {} bill_dict[1] = bill_overview['electricity'] # electricity_bill bill_dict[2] = bill_overview['gas'] # gas_bill @@ -116,8 +113,81 @@ class Income_Statement(): self.net_non_energy_opex = None self.total_opex = None self.noi = None + # energy_debt_service = + # other_debt_service = + # total_debt_service = + # bill_saving = + # energy_DSCR = + # total_DSCR = + + def put_hist(self, year, income_input, annual_bill_table): + """ + Put historical income statement data and generate a single year income statement + Inputs are incomplete income statement items, and energy bill overview + Final output is an single year income_statement objectvie with standarized items filled + + Args: + year (int): the year of this income statement + income_input (dictionary): 3 elements of inputs + annual_bill_table (dictionary): annual bill, for 4 utility_types + + Description Sample: + income_input = {revenue': 100000, 'utility_expense': 60000,'non_utility_expense': 3000} + annual_bill_table = {'electricity': electricity_bill, 'oil': oil_bill, 'gas': gas_bill, 'water': water_bill} + """ + self.year = year + self.revenue = income_input['revenue'] + self.utility_expense = income_input['utility_expense'] + self.non_utility_expense = income_input['non_utility_expense'] + self.electricity_opex = annual_bill_table['electricity'][self.year] + self.gas_opex = annual_bill_table['gas'][self.year] + self.oil_opex = annual_bill_table['oil'][self.year] + self.water_opex = annual_bill_table['water'][self.year] + self.energy_opex = self.electricity_opex + self.oil_opex + self.gas_opex + self.water_opex + self.other_utility = self.utility_expense - self.energy_opex + self.net_non_energy_opex = self.other_utility + self.non_utility_expense + self.total_opex = self.energy_opex + self.net_non_energy_opex + self.noi = self.revenue - self.total_opex # net operating income + + def put_average(self, average_revenue, annual_bills, characters): + """ + Calculate and create average income statement. + Revenue, energy bills are average of historical data. + Other items are calculated by historical characters. + Args: + average_revenue (float): average revenue calculated from historical income statements + annual_bills (dictionary): dictionary of float value of average annual bills, for 4 utility_types + characters (dictionary): 6 characters calculated from historical income statements + Final instance is a single year income statement without named year + + Description: + characters = { + 'start_year': (int) 2014, + 'end_year': (int) 2016, + 'cagr': (float) 0.054, + 'other_utility_percent': (float) 0.020 == 2.0% + 'non_utility_expense_percent': (float) 0.030 == 3.0% + 'revenue_average': (float) 3000.00 + } + annual_bills = {'electricity': 100.0, 'oil': 200.0, 'gas': 300.0, 'water': 400.0} + """ + self.revenue = average_revenue + self.electricity_opex = annual_bills['electricity'] + self.gas_opex = annual_bills['gas'] + self.oil_opex = annual_bills['oil'] + self.water_opex = annual_bills['water'] + self.energy_opex = self.electricity_opex + self.gas_opex + self.oil_opex + self.water_opex + self.other_utility = self.revenue * characters['other_utility_percent'] + self.non_utility_expense = self.revenue * characters['non_utility_expense_percent'] + self.utility_expense = self.energy_opex + self.other_utility + self.net_non_energy_opex = self.other_utility + self.non_utility_expense + self.total_opex = self.energy_opex + self.net_non_energy_opex + self.noi = self.revenue - self.total_opex def assign_next(self, income_statement_characters, bill_overview_organized): + """ + !!! this function will be deleted in the next version!! + """ self.electricity_opex = bill_overview_organized['electricity'][self.year] self.gas_opex = bill_overview_organized['gas'][self.year] self.oil_opex = bill_overview_organized['oil'][self.year] @@ -145,6 +215,13 @@ class Income_Statement(): def convert_income_statement_class(income_statement_class): + """ + Convert single year income statement objective into a dictionary format + Args: + income_statement_class (objective): single year income statement objective + Return: + income_statement_dict (dictionary) + """ income_statement_dict = { 'year': income_statement_class.year, 'revenue': income_statement_class.revenue, @@ -160,3 +237,300 @@ def convert_income_statement_class(income_statement_class): 'total_opex': income_statement_class.total_opex, 'noi': income_statement_class.noi} return income_statement_dict + + +class Income_Statement_Next(): + """ + Create single year income_statement objective, + with input of last year data, income statement characters, and projected annual bill + """ + def __init__(self, year, last_revenue, growth_rate_flag, characters, annual_bill_table): + """ + Calculation is done in initiation. + Args: + year (int): the year that will be projected on + last_revenue (float): last year revenue + growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average + characters (dictionary): 6 characters calculated from historical income statements + annual_bill_table (dictionary): dictionary of dictionary of annual bills, for 4 utility_types + Final instance is a single year income_statement objective + + Description: + characters = { + 'start_year': (int) 2014, + 'end_year': (int) 2016, + 'cagr': (float) 0.054, + 'other_utility_percent': (float) 0.020 == 2.0% + 'non_utility_expense_percent': (float) 0.035 == 3.5% + 'revenue_average': (float) 3000.00 + } + annual_bill_table = {'electricity': electricity_bill, 'oil': oil_bill, 'gas': gas_bill, 'water': water_bill} + electricity_bill = {2014: 100, 2015:200, ...} + """ + self.year = year + + if growth_rate_flag == -1.0: # growth_rate_flag == average + current_revenue = characters['revenue_average'] + else: + if growth_rate_flag == -2.0: # growth_rate_flag == cagr + growth_rate = characters['cagr'] + else: + growth_rate = growth_rate_flag # growth_rate_flag == 0.00, 0.01, ... + current_revenue = last_revenue * (1 + growth_rate) + self.revenue = current_revenue + self.electricity_opex = annual_bill_table['electricity'][self.year] + self.gas_opex = annual_bill_table['gas'][self.year] + self.oil_opex = annual_bill_table['oil'][self.year] + self.water_opex = annual_bill_table['water'][self.year] + self.energy_opex = self.electricity_opex + self.gas_opex + self.oil_opex + self.water_opex + self.other_utility = self.revenue * characters['other_utility_percent'] + self.non_utility_expense = self.revenue * characters['non_utility_expense_percent'] + self.utility_expense = self.energy_opex + self.other_utility + self.net_non_energy_opex = self.other_utility + self.non_utility_expense + self.total_opex = self.energy_opex + self.net_non_energy_opex + self.noi = self.revenue - self.total_opex + + +class Income_Statement_Table(): + """ + Create a income statement table, containing multiple years, including historical and projected data + Key Attributes are: + hist_table (list): list of single year income_statement objectives, containing historical data + table(list): list of single year income_statement objectives, containing historical and projected data + """ + def __init__(self, raw_income_input, annual_bill_table): + """ + Create hist_table to store historical income statement data, and calculate some key characters. + Args: + raw_income_input (dictionary): dict of dict of incomplete income statement, for historical years. Key = year + annual_bill_table (dictionary): dictionary of dictionary of annual bills, for 4 utility_types + + Key Attributes: + cagr (float): compound annual growth rate + other_utility_percent (float): percentage, (average other_utility / average revenue) + non_utility_expense_percent (float): percentage, (average non_utility_expense_percent / average revenue) + hist_table (list): list of single year income_statement objectives, containing historical data + table(list): list of single year income_statement objectives, containing historical and projected data + characters (dictionary): contains key characters determined from historical data. Is used to project + + Description: + raw_income_input = {2014: {'revenue': 90.0, 'utility_expense': 55.0, 'non_utility_expense': 35.0}, 2015:{},} + annual_bill_table = {'electricity': electricity_bill, 'oil': oil_bill, 'gas': gas_bill, 'water': water_bill} + electricity_bill = {2014: 100, 2015:200, ...} + characters = { + 'start_year': (int) 2014, + 'end_year': (int) 2016, + 'cagr': (float) 0.054, + 'other_utility_percent': (float) 0.02, + 'non_utility_expense_percent': (float) 0.03, + 'revenue_average': (float) 3000.00 + } + """ + self.hist_start_year = None + self.hist_end_year = None + self.cagr = 0.00 + self.other_utility_percent = 0.00 + self.non_utility_expense_percent = 0.00 + self.revenue_average = 0.00 # average revenue + self.hist_table = [] + self.table = [] + self.characters = {} + + for year in raw_income_input: + current_income_statement = Income_Statement() + current_income_statement.put_hist(year, raw_income_input[year], annual_bill_table) + self.hist_table.append(current_income_statement) + + sorted_income_hist_year = sorted(raw_income_input) + self.hist_start_year = sorted_income_hist_year[0] + self.hist_end_year = sorted_income_hist_year[-1] + # if start_year == None: return None + self.cagr = lib.cal_cagr( + raw_income_input[self.hist_start_year]['revenue'], + raw_income_input[self.hist_end_year]['revenue'], + self.hist_end_year - self.hist_start_year) + + revenue_sum = 0 + other_utility_sum = 0 + non_utility_expense_sum = 0 + for current_income_statement in self.hist_table: + revenue_sum += current_income_statement.revenue + other_utility_sum += current_income_statement.other_utility + non_utility_expense_sum += current_income_statement.non_utility_expense + + self.other_utility_percent = other_utility_sum/revenue_sum + self.non_utility_expense_percent = non_utility_expense_sum/revenue_sum + self.revenue_average = revenue_sum / (self.hist_end_year - self.hist_start_year + 1) + + self.table = self.hist_table + self.characters = { + 'start_year': self.hist_start_year, + 'end_year': self.hist_end_year, + 'cagr': self.cagr, + 'other_utility_percent': self.other_utility_percent, + 'non_utility_expense_percent': self.non_utility_expense_percent, + 'revenue_average': self.revenue_average} + + def project(self, growth_rate_flag, analysis_date, annual_bill_table): + """ + Project future income statement. Append multiple single year income_statement objectives to self.table + Args: + growth_rate_flag (float): indicating assumed growth rate, -2.0 == cagr, -1.0 == historical average + analysis_date (dictionary): proforma's starting date and the years of proforma + annual_bill_table (dictionary): dictionary of dictionary of annual bills, for 4 utility_types + Return: + list: list of single year income_statement objectives, containing historical and projection + Note: + project() overwrites existing projection data + """ + # characters = copy.deepcopy(self.characters) + proforma_year = form_bill_year(analysis_date['proforma_start'], + analysis_date['proforma_duration']) + current_table = copy.deepcopy(self.hist_table) + for year in proforma_year: + last_revenue = current_table[-1].revenue + if year <= current_table[-1].year: + continue + current_income_statement = Income_Statement_Next( + year, last_revenue, growth_rate_flag, self.characters, annual_bill_table) + current_table.append(current_income_statement) + # print(current_income_statement.revenue, current_income_statement.noi) + self.table = current_table + return current_table + + def get_hist_table(self): + """ + Get historical table, in dictionary formatting + Return: + dictionary: dict of dict of income statement. Key is year + Description: + hist_table_dict = {2014: {'year': 2014, 'revenue': 100.0, ..., 'not': 5.0}, ... , 2016:{}} + """ + hist_table_dict = {} + for current_income_statement in self.hist_table: + hist_table_dict[current_income_statement.year] = { + 'year': current_income_statement.year, + 'revenue': current_income_statement.revenue, + 'utility_expense': current_income_statement.utility_expense, + 'energy_opex': current_income_statement.energy_opex, + 'electricity_opex': current_income_statement.electricity_opex, + 'gas_opex': current_income_statement.gas_opex, + 'oil_opex': current_income_statement.oil_opex, + 'water_opex': current_income_statement.water_opex, + 'other_utility': current_income_statement.other_utility, + 'non_utility_expense': current_income_statement.non_utility_expense, + 'net_non_energy_opex': current_income_statement.net_non_energy_opex, + 'total_opex': current_income_statement.total_opex, + 'noi': current_income_statement.noi} + return hist_table_dict + + def get_cagr(self): + """ + Get compound annual growth rate + Return: + float: compound annual growth rate + """ + return copy.deepcopy(self.cagr) + + def get_average(self): + """ + Get average income statement. Only need initialized Income_Statement_Table + Return: + dictionary: dict of average income statement, Keys are income statement items. + Description: + output_dict = {'year': None, 'revenue': 95000.0, ... ,'noi': 35166.666666666657} + """ + # current_year = 'Average' + current_income_statement = Income_Statement() + average_revenue = mean(list(i_s.revenue for i_s in self.hist_table)) + annual_bills = { + 'electricity': mean(list(i_s.electricity_opex for i_s in self.hist_table)), + 'gas': mean(list(i_s.gas_opex for i_s in self.hist_table)), + 'oil': mean(list(i_s.oil_opex for i_s in self.hist_table)), + 'water': mean(list(i_s.water_opex for i_s in self.hist_table)) + } + current_income_statement.put_average(average_revenue, annual_bills, self.characters) + + return convert_income_statement_class(current_income_statement) + + def get_single_year(self, year): + """ + Get single year income statement from self.table. Table can either contain or not contain projection + Args: + year (int): the year that need to be extracted + Return: + dict: single year income statement. Keys are income statement items. + Note: + if the target year is not in self.table, then return None. + """ + for current_income_statement in self.table: + if current_income_statement.year == year: + return { + 'year': current_income_statement.year, + 'revenue': current_income_statement.revenue, + 'utility_expense': current_income_statement.utility_expense, + 'energy_opex': current_income_statement.energy_opex, + 'electricity_opex': current_income_statement.electricity_opex, + 'gas_opex': current_income_statement.gas_opex, + 'oil_opex': current_income_statement.oil_opex, + 'water_opex': current_income_statement.water_opex, + 'other_utility': current_income_statement.other_utility, + 'non_utility_expense': current_income_statement.non_utility_expense, + 'net_non_energy_opex': current_income_statement.net_non_energy_opex, + 'total_opex': current_income_statement.total_opex, + 'noi': current_income_statement.noi} + return None + + def get_noi_dict(self): + """ + Get a dictionary of net operating income. + Return: + dictionary: net operating income for each year in income statement table. Key is year + """ + noi_dict = {} + for current_income_statement in self.table: + noi_dict[current_income_statement.year] = current_income_statement.noi + return noi_dict + + +# ************ ugly tests*********** +# IS_table = Income_Statement_Table(db.raw_income_input, db.bill_overview_organized) +# IS_table.project(-1.0, db.analysis_date, db.bill_overview_organized) +# assert IS_table.get_hist_table() == db.income_statement_full # test get_hist_table +# print('\n cagr =', IS_table.get_cagr()) # test get_cagr +# print('\n average hist =', IS_table.get_average()) # test get_average +# print('\n noi dict =', IS_table.get_noi_dict()) # test get_noi_dict +# print('\n 2017 income statement =', IS_table.get_single_year(2017)) # test get_single_year + +# ************ test draft*********** +# class test_stru(): +# def __init__(self, number): +# self.number = number + +# test_1 = test_stru(2.0) +# test_2 = test_stru(3.0) +# test_3 = test_stru(5.0) + +# test_list = [test_1, test_2, test_3] +# form_list = list(ob.number for ob in test_list) +# print(form_list) + + +# def get_income_statement_single_year(self, year, income_statement_table): +# for current_income_statement in income_statement_table: +# if current_income_statement.year == year: +# return { +# 'year': current_income_statement.year, +# 'revenue': current_income_statement.revenue, +# 'utility_expense': current_income_statement.utility_expense, +# 'energy_opex': current_income_statement.energy_opex, +# 'electricity_opex': current_income_statement.electricity_opex, +# 'gas_opex': current_income_statement.gas_opex, +# 'oil_opex': current_income_statement.oil_opex, +# 'water_opex': current_income_statement.water_opex, +# 'other_utility': current_income_statement.other_utility, +# 'non_utility_expense': current_income_statement.non_utility_expense, +# 'net_non_energy_opex': current_income_statement.net_non_energy_opex, +# 'total_opex': current_income_statement.total_opex, +# 'noi': current_income_statement.noi} +# return None diff --git a/bpfin/financials/income_statement_form_hist.py b/bpfin/financials/income_statement_form_hist.py index 65ba995d5bca40702883dba1b5d4a53cedaba6f5..8e63bba2a2dbc53ba0ea0704c858579299a4aaa2 100644 --- a/bpfin/financials/income_statement_form_hist.py +++ b/bpfin/financials/income_statement_form_hist.py @@ -4,6 +4,7 @@ from bpfin.lib import other as lib def income_statement_character(income_statement_hist): """Determine annual growth rate, other_utility_percentage, non_utility_expense_percentage + !!!! This function should be deleted or rephrased in next version !! Args: income_statement_hist (dictionary): full income statement, with all items filled, for available years Return: @@ -43,6 +44,7 @@ def income_statement_character(income_statement_hist): def form_income_statement_hist(raw_income_input, bill_overview_organized, analysis_date): """ form income statement table with raw inputs from UI, and organized bill_overview. NO projection + !!!! This function should be deleted or rephrased in next version !! Args: raw_income_input (dictionary): dictionary of dictionary. raw inputs for income statement for available years bill_overview_organized (dictionary): dict of dict, 4 utility types, with blank charge filled with average diff --git a/bpfin/financials/income_statement_next.py b/bpfin/financials/income_statement_next.py index d46c4ab597d2e33ac61936f4a18b92cb466225a5..b619049b2ee856850b7479b3938e991d658a84d0 100644 --- a/bpfin/financials/income_statement_next.py +++ b/bpfin/financials/income_statement_next.py @@ -7,6 +7,7 @@ from bpfin.financials.income_statement_form_hist import income_statement_charact def income_statement_next(income_statement_hist, bill_overview_organized, growth_rate_flag): """Project income statement for UI input. inputs are whatever bill and financial statements are available + !!!! This function should be deleted or rephrased in next version !! Args: income_statement_hist (dictionary): historical income statement, with all items filled, for available years bill_overview_organized (dictionary): annual bills for 4 utility types, with blank cells filled diff --git a/bpfin/financials/income_statement_projection.py b/bpfin/financials/income_statement_projection.py index ac4d5890a4eaf37b81a079ae88ecf17a41106b52..221225d903f4c66d6ce2aa39893dd887a594e528 100644 --- a/bpfin/financials/income_statement_projection.py +++ b/bpfin/financials/income_statement_projection.py @@ -8,6 +8,7 @@ import pprint def income_statement_projection(income_statement_hist, bill_overview_organized, growth_rate_flag, analysis_date): """ + !!!! This function should be deleted or rephrased in next version !! Project income statement tp future multiple years. inputs are historical income statements and annual bills. Args: income_statement_hist (dictionary): historical income statement, with all items filled, for available years diff --git a/bpfin/tests/test_financials/test_financial_lib.py b/bpfin/tests/test_financials/test_financial_lib.py index b756a240da12420cb5347957009d29eb7e515ba6..3d9c6e6deb499a386dd5f821c3938bb5a949ec70 100644 --- a/bpfin/tests/test_financials/test_financial_lib.py +++ b/bpfin/tests/test_financials/test_financial_lib.py @@ -2,6 +2,7 @@ import datetime from bpfin.financials import financial_lib as fl from bpfin.tests.testdata import sample_data as db from bpfin.financials.income_statement_form_hist import form_income_statement_hist +from bpfin.financials.financial_lib import Income_Statement_Table def test_organize_bill_overview(): @@ -20,4 +21,21 @@ def test_income_statement_single_year(): assert output_dict == result_dict +def test_Income_Statement_Table(): + input_raw_income_input = db.raw_income_input + input_annual_bill_table = db.bill_overview_organized + output_cagr = 0.05409255338945984 + output_hist_table = db.income_statement_full + output_average_table = db.income_statement_average + output_noi_dict = db.noi_dict_average + output_single_year = db.income_statement_2017_avg + + IS_table = Income_Statement_Table(input_raw_income_input, input_annual_bill_table) + IS_table.project(-1.0, db.analysis_date, db.bill_overview_organized) + + assert IS_table.get_cagr() == output_cagr # test get_cagr + assert IS_table.get_hist_table() == output_hist_table # test get_hist_table + assert IS_table.get_average() == output_average_table # test get_average + assert IS_table.get_noi_dict() == output_noi_dict # test get_noi_dict + assert IS_table.get_single_year(2017) == output_single_year # test get_single_year diff --git a/bpfin/tests/testdata/sample_data.py b/bpfin/tests/testdata/sample_data.py index b7a8053a466e0858a5970b982fc59c5081c47844..45f721ae5859e3bec54e881e799d6f0c829f2c51 100644 --- a/bpfin/tests/testdata/sample_data.py +++ b/bpfin/tests/testdata/sample_data.py @@ -192,6 +192,26 @@ income_statement_full = { 'total_opex': 63000.0, 'noi': 37000.0} } +income_statement_average = { + 'year': None, 'revenue': 95000.0, 'utility_expense': 56666.666666666672, 'energy_opex': 35880.343493666973, + 'electricity_opex': 33561.454604778082, 'gas_opex': 1164.4444444444443, 'oil_opex': 1154.4444444444443, + 'water_opex': 0.0, 'other_utility': 20786.323172999699, 'non_utility_expense': 3166.6666666666665, + 'net_non_energy_opex': 23952.989839666367, 'total_opex': 59833.333333333343, 'noi': 35166.666666666657} + +noi_dict_average = { + 2014: 31500.0, 2015: 37000.0, 2016: 37000.0, 2017: 33493.73724516164, 2018: 33165.377210606544, + 2019: 32298.852732150932, 2020: 31450.146124428546, 2021: 30663.261480034096, 2022: 29946.57845606045, + 2023: 29242.418277114877, 2024: 28522.26581429284, 2025: 27777.914231279196, 2026: 26974.67650825437, + 2027: 26110.42490293656, 2028: 25225.108030085234, 2029: 24339.044697870617, 2030: 23452.675696890437, + 2031: 22563.678164016266, 2032: 21664.038064983062, 2033: 20743.76458479633, 2034: 19798.79239963583, + 2035: 18828.088043102543, 2036: 17831.510509478205} + +income_statement_2017_avg = { + 'year': 2017, 'revenue': 95000.0, 'utility_expense': 58339.59608817169, 'energy_opex': 37553.27291517199, + 'electricity_opex': 34523.27291517199, 'gas_opex': 1520, 'oil_opex': 1510, 'water_opex': 0, + 'other_utility': 20786.3231729997, 'non_utility_expense': 3166.6666666666665, + 'net_non_energy_opex': 23952.989839666367, 'total_opex': 61506.26275483836, 'noi': 33493.73724516164} + # income_input_next = { # 'year': 2017, 'revenue': 105409.25533894598, 'utility_expense': 60617.176566756985, 'energy_opex': 37553.27291517199, # 'electricity_opex': 34523.27291517199, 'gas_opex': 1520, 'oil_opex': 1510, 'water_opex': 0,