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():