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:
parent
9cdf626bdd
commit
fe26bf8a6a
23
main.py
23
main.py
@ -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)
|
||||||
|
|||||||
@ -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")]
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user