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.
This commit is contained in:
Luís Murta 2021-02-02 21:57:07 +00:00
parent 9cdf626bdd
commit fe26bf8a6a
Signed by: satprog
GPG Key ID: DDF2EFC6179009DC
4 changed files with 229 additions and 26 deletions

23
main.py
View File

@ -2,7 +2,7 @@ from pathlib import Path
import argparse import argparse
import datetime as dt import datetime as dt
from pfbudget.graph import monthly from pfbudget.graph import average, discrete, monthly
from pfbudget.transactions import load_transactions, save_transactions from pfbudget.transactions import load_transactions, save_transactions
import pfbudget.tools as tools import pfbudget.tools as tools
@ -123,7 +123,6 @@ def vacation(state, args):
date(2019, 12, 23), date(2020, 1, 2) date(2019, 12, 23), date(2020, 1, 2)
date(2020, 7, 1), date(2020, 7, 30) date(2020, 7, 1), date(2020, 7, 30)
""" """
print(args)
if args.option == "list": if args.option == "list":
print(state.vacations) print(state.vacations)
elif args.option == "remove": elif args.option == "remove":
@ -144,7 +143,10 @@ def status(state, args):
def graph(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__": if __name__ == "__main__":
@ -168,11 +170,9 @@ if __name__ == "__main__":
p_init.add_argument("raw", help="the raw data dir") p_init.add_argument("raw", help="the raw data dir")
p_init.add_argument("data", help="the parsed 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("--raw", help="new raw data dir")
p_restart.add_argument("--data", help="new parsed data dir") p_restart.add_argument("--data", help="new parsed data dir")
p_restart.set_defaults(func=restart)
p_backup.add_argument( p_backup.add_argument(
"option", "option",
@ -197,6 +197,19 @@ if __name__ == "__main__":
"pos", help="position of vacation to remove", type=int, nargs=1 "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_backup.set_defaults(func=backup)
p_parse.set_defaults(func=parse) p_parse.set_defaults(func=parse)
p_vacation.set_defaults(func=vacation) p_vacation.set_defaults(func=vacation)

View File

@ -48,11 +48,15 @@ def get_income_categories():
def get_fixed_expenses(): def get_fixed_expenses():
return [Utilities.name, Commute.name] return [Utilities.name]
def get_required_expenses(): def get_required_expenses():
return [Groceries.name] return [Groceries.name, Commute.name]
def get_health_expenses():
return [Medical.name]
def get_discretionary_expenses(): def get_discretionary_expenses():
@ -64,6 +68,7 @@ def get_discretionary_expenses():
*get_income_categories(), *get_income_categories(),
*get_fixed_expenses(), *get_fixed_expenses(),
*get_required_expenses(), *get_required_expenses(),
*get_health_expenses(),
Investment.name, Investment.name,
Null.name, Null.name,
] ]
@ -212,3 +217,8 @@ class Investment(Categories):
name = "Investment" name = "Investment"
regex = [c("subscrition")] regex = [c("subscrition")]
banks = ["BankC"] banks = ["BankC"]
class Medical(Categories):
name = "Medical"
regex = [c("hospital", "pharmacy")]

View File

@ -5,26 +5,33 @@ from .categories import (
get_income_categories, get_income_categories,
get_fixed_expenses, get_fixed_expenses,
get_required_expenses, get_required_expenses,
get_health_expenses,
get_discretionary_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): def monthly(state, start, end):
transactions = load_transactions(state.data_dir) 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) income, fixed, required, health, discretionary = [], [], [], [], []
monthly_transactions_by_categories = {} monthly_transactions_by_categories = by_month_and_category(transactions, start, end)
income, fixed, required, discretionary = [], [], [], []
for month, transactions in monthly_transactions.items(): for _, transactions_by_category in monthly_transactions_by_categories.items():
monthly_transactions_by_categories[month] = by_category(transactions)
income.append( income.append(
sum( sum(
float(t.value) float(t.value)
for category, transactions in monthly_transactions_by_categories[ for category, transactions in transactions_by_category.items()
month
].items()
if transactions and category in get_income_categories() if transactions and category in get_income_categories()
for t in transactions for t in transactions
) )
@ -32,9 +39,7 @@ def monthly(state, start, end):
fixed.append( fixed.append(
sum( sum(
-float(t.value) -float(t.value)
for category, transactions in monthly_transactions_by_categories[ for category, transactions in transactions_by_category.items()
month
].items()
if transactions and category in get_fixed_expenses() if transactions and category in get_fixed_expenses()
for t in transactions for t in transactions
) )
@ -42,19 +47,23 @@ def monthly(state, start, end):
required.append( required.append(
sum( sum(
-float(t.value) -float(t.value)
for category, transactions in monthly_transactions_by_categories[ for category, transactions in transactions_by_category.items()
month
].items()
if transactions and category in get_required_expenses() if transactions and category in get_required_expenses()
for t in transactions 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( discretionary.append(
sum( sum(
-float(t.value) -float(t.value)
for category, transactions in monthly_transactions_by_categories[ for category, transactions in transactions_by_category.items()
month
].items()
if transactions and category in get_discretionary_expenses() if transactions and category in get_discretionary_expenses()
for t in transactions for t in transactions
) )
@ -65,8 +74,166 @@ def monthly(state, start, end):
daterange(start, end, "month"), daterange(start, end, "month"),
fixed, fixed,
required, required,
health,
discretionary, discretionary,
labels=["Fixed", "Required", "Discretionary"], labels=["Fixed", "Required", "Health", "Discretionary"],
) )
plt.legend(loc="upper left") plt.legend(loc="upper left")
plt.show() 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()

View File

@ -127,6 +127,9 @@ class Transactions(list):
def daterange(start, end, period): def daterange(start, end, period):
if not start or not end:
raise TransactionError("daterange requires start and end")
if period == "year": if period == "year":
r = [d.strftime("%Y") for d in rrule(YEARLY, dtstart=start, until=end)] r = [d.strftime("%Y") for d in rrule(YEARLY, dtstart=start, until=end)]
elif period == "month": elif period == "month":
@ -179,6 +182,16 @@ def by_category(transactions) -> dict:
return transactions_by_category 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: def load_transactions(data_dir) -> Transactions:
transactions = Transactions() transactions = Transactions()
for df in Path(data_dir).iterdir(): for df in Path(data_dir).iterdir():