From fe26bf8a6ad116163a05b700e1ffb9bbbe211f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Tue, 2 Feb 2021 21:57:07 +0000 Subject: [PATCH] Adds two new plot methods and health expenses Two new graphs created, discrete, where all discretionary expenses are plotted, and average, for the yearly average. This later one will be moved to report form. Health expenses separated from required. Commute moved from fixed to required expenses. New by_month_and_category functions added to transactions.py. --- main.py | 23 ++++- pfbudget/categories.py | 14 ++- pfbudget/graph.py | 205 +++++++++++++++++++++++++++++++++++---- pfbudget/transactions.py | 13 +++ 4 files changed, 229 insertions(+), 26 deletions(-) diff --git a/main.py b/main.py index 99cef34..a7b33bf 100644 --- a/main.py +++ b/main.py @@ -2,7 +2,7 @@ from pathlib import Path import argparse import datetime as dt -from pfbudget.graph import monthly +from pfbudget.graph import average, discrete, monthly from pfbudget.transactions import load_transactions, save_transactions import pfbudget.tools as tools @@ -123,7 +123,6 @@ def vacation(state, args): date(2019, 12, 23), date(2020, 1, 2) date(2020, 7, 1), date(2020, 7, 30) """ - print(args) if args.option == "list": print(state.vacations) elif args.option == "remove": @@ -144,7 +143,10 @@ def status(state, args): def graph(state, args): - monthly(state, start=dt.date(2020, 1, 1), end=dt.date(2020, 12, 31)) + if args.option == "monthly": + monthly(state, args.start, args.end) + elif args.option == "discrete": + discrete(state, args.start, args.end) if __name__ == "__main__": @@ -168,11 +170,9 @@ if __name__ == "__main__": p_init.add_argument("raw", help="the raw data dir") p_init.add_argument("data", help="the parsed data dir") - p_init.set_defaults(func=init) p_restart.add_argument("--raw", help="new raw data dir") p_restart.add_argument("--data", help="new parsed data dir") - p_restart.set_defaults(func=restart) p_backup.add_argument( "option", @@ -197,6 +197,19 @@ if __name__ == "__main__": "pos", help="position of vacation to remove", type=int, nargs=1 ) + p_graph.add_argument( + "option", + type=str, + choices=["monthly", "discrete"], + nargs="?", + default="monthly", + help="graph option help", + ) + p_graph.add_argument("start", type=str, nargs="?", help="graph start date") + p_graph.add_argument("end", type=str, nargs="?", help="graph end date") + + p_init.set_defaults(func=init) + p_restart.set_defaults(func=restart) p_backup.set_defaults(func=backup) p_parse.set_defaults(func=parse) p_vacation.set_defaults(func=vacation) diff --git a/pfbudget/categories.py b/pfbudget/categories.py index 05acb18..2e434c5 100644 --- a/pfbudget/categories.py +++ b/pfbudget/categories.py @@ -48,11 +48,15 @@ def get_income_categories(): def get_fixed_expenses(): - return [Utilities.name, Commute.name] + return [Utilities.name] def get_required_expenses(): - return [Groceries.name] + return [Groceries.name, Commute.name] + + +def get_health_expenses(): + return [Medical.name] def get_discretionary_expenses(): @@ -64,6 +68,7 @@ def get_discretionary_expenses(): *get_income_categories(), *get_fixed_expenses(), *get_required_expenses(), + *get_health_expenses(), Investment.name, Null.name, ] @@ -212,3 +217,8 @@ class Investment(Categories): name = "Investment" regex = [c("subscrition")] banks = ["BankC"] + + +class Medical(Categories): + name = "Medical" + regex = [c("hospital", "pharmacy")] diff --git a/pfbudget/graph.py b/pfbudget/graph.py index 311ae40..601c804 100644 --- a/pfbudget/graph.py +++ b/pfbudget/graph.py @@ -5,26 +5,33 @@ from .categories import ( get_income_categories, get_fixed_expenses, get_required_expenses, + get_health_expenses, get_discretionary_expenses, ) -from .transactions import load_transactions, daterange, by_category, by_month +from .transactions import ( + load_transactions, + daterange, + by_category, + by_month, + by_month_and_category, +) def monthly(state, start, end): transactions = load_transactions(state.data_dir) + if not start: + start = transactions[0].date + if not end: + end = transactions[-1].date - monthly_transactions = by_month(transactions, start, end) - monthly_transactions_by_categories = {} - income, fixed, required, discretionary = [], [], [], [] + income, fixed, required, health, discretionary = [], [], [], [], [] + monthly_transactions_by_categories = by_month_and_category(transactions, start, end) - for month, transactions in monthly_transactions.items(): - monthly_transactions_by_categories[month] = by_category(transactions) + for _, transactions_by_category in monthly_transactions_by_categories.items(): income.append( sum( float(t.value) - for category, transactions in monthly_transactions_by_categories[ - month - ].items() + for category, transactions in transactions_by_category.items() if transactions and category in get_income_categories() for t in transactions ) @@ -32,9 +39,7 @@ def monthly(state, start, end): fixed.append( sum( -float(t.value) - for category, transactions in monthly_transactions_by_categories[ - month - ].items() + for category, transactions in transactions_by_category.items() if transactions and category in get_fixed_expenses() for t in transactions ) @@ -42,19 +47,23 @@ def monthly(state, start, end): required.append( sum( -float(t.value) - for category, transactions in monthly_transactions_by_categories[ - month - ].items() + for category, transactions in transactions_by_category.items() if transactions and category in get_required_expenses() for t in transactions ) ) + health.append( + sum( + -float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_health_expenses() + for t in transactions + ) + ) discretionary.append( sum( -float(t.value) - for category, transactions in monthly_transactions_by_categories[ - month - ].items() + for category, transactions in transactions_by_category.items() if transactions and category in get_discretionary_expenses() for t in transactions ) @@ -65,8 +74,166 @@ def monthly(state, start, end): daterange(start, end, "month"), fixed, required, + health, discretionary, - labels=["Fixed", "Required", "Discretionary"], + labels=["Fixed", "Required", "Health", "Discretionary"], ) plt.legend(loc="upper left") plt.show() + + +def discrete(state, start, end): + transactions = load_transactions(state.data_dir) + if not start: + start = transactions[0].date + if not end: + end = transactions[-1].date + + income, fixed, required, health, discretionary = [], [], [], [], [] + monthly_transactions_by_categories = by_month_and_category(transactions, start, end) + + for _, transactions_by_category in monthly_transactions_by_categories.items(): + income.append( + sum( + float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_income_categories() + for t in transactions + ) + ) + fixed.append( + sum( + -float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_fixed_expenses() + for t in transactions + ) + ) + required.append( + sum( + -float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_required_expenses() + for t in transactions + ) + ) + health.append( + sum( + -float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_health_expenses() + for t in transactions + ) + ) + d = [] + for category, transactions in transactions_by_category.items(): + if category in get_discretionary_expenses(): + try: + d.append(sum(-float(t.value) for t in transactions)) + except TypeError: + d.append(0) + + discretionary.append(d) + + # transposing discretionary + discretionary = list(map(list, zip(*discretionary))) + + plt.plot(daterange(start, end, "month"), income, label="Income") + plt.stackplot( + daterange(start, end, "month"), + fixed, + required, + health, + *discretionary, + labels=["Fixed", "Required", "Health", *get_discretionary_expenses()], + ) + plt.legend(loc="upper left") + plt.show() + + +def average(state, start, end): + transactions = load_transactions(state.data_dir) + + income, fixed, required, health, discretionary = [], [], [], [], [] + monthly_transactions_by_categories = by_month_and_category(transactions, start, end) + + for _, transactions_by_category in monthly_transactions_by_categories.items(): + income.append( + sum( + float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_income_categories() + for t in transactions + ) + ) + fixed.append( + sum( + -float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_fixed_expenses() + for t in transactions + ) + ) + required.append( + sum( + -float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_required_expenses() + for t in transactions + ) + ) + health.append( + sum( + -float(t.value) + for category, transactions in transactions_by_category.items() + if transactions and category in get_health_expenses() + for t in transactions + ) + ) + d = [] + for category, transactions in transactions_by_category.items(): + if category in get_discretionary_expenses(): + try: + d.append(sum(-float(t.value) for t in transactions)) + except TypeError: + d.append(0) + + discretionary.append(d) + + # transposing discretionary + discretionary = list(map(list, zip(*discretionary))) + + print(discretionary) + + n = len(daterange(start, end, "month")) + + avg_income = sum(income) / n + + l_avg_income = [avg_income] * n + + avg_fixed = [sum(fixed) / n] * n + avg_required = [sum(required) / n] * n + avg_health = [sum(health) / n] * n + avg_discretionary = [[sum(d) / n] * n for d in discretionary] + + print(avg_discretionary) + + plt.plot(daterange(start, end, "month"), l_avg_income, label=f"Income {avg_income}") + plt.stackplot( + daterange(start, end, "month"), + avg_fixed, + avg_required, + avg_health, + *avg_discretionary, + labels=[ + f"Fixed {avg_fixed[0]/avg_income * 100}%", + f"Required {avg_required[0]/avg_income * 100}%", + f"Health {avg_health[0]/avg_income * 100}%", + *[ + f"{e} {avg_discretionary[i][0]/avg_income * 100}%" + for i, e in enumerate(get_discretionary_expenses()) + ], + ], + ) + plt.legend(bbox_to_anchor=(1, 1), loc="upper left") + plt.show() diff --git a/pfbudget/transactions.py b/pfbudget/transactions.py index 7e28a4b..975b92c 100644 --- a/pfbudget/transactions.py +++ b/pfbudget/transactions.py @@ -127,6 +127,9 @@ class Transactions(list): def daterange(start, end, period): + if not start or not end: + raise TransactionError("daterange requires start and end") + if period == "year": r = [d.strftime("%Y") for d in rrule(YEARLY, dtstart=start, until=end)] elif period == "month": @@ -179,6 +182,16 @@ def by_category(transactions) -> dict: return transactions_by_category +def by_month_and_category(transactions, start, end) -> dict: + monthly_transactions_by_categories = {} + + monthly_transactions = by_month(transactions, start, end) + for month, transactions in monthly_transactions.items(): + monthly_transactions_by_categories[month] = by_category(transactions) + + return monthly_transactions_by_categories + + def load_transactions(data_dir) -> Transactions: transactions = Transactions() for df in Path(data_dir).iterdir():