I am trying to generate a chart that shows the top 5 spending categories.
I've got it working up to a certain point and then it says "matplotlib does not support generators as input". I am pretty new to python in general but am trying to learn more about it.
Up to this point in the code it works:
import Expense
import collections
import matplotlib.pyplot as plt
expenses = Expense.Expenses()
expenses.read_expenses(r"C:\Users\budget\data\spending_data.csv")
spending_categories = []
for expense in expenses.list:
spending_categories.append(expense.category)
spending_counter = collections.Counter(spending_categories)
top5 = spending_counter.most_common(5)
If you did a print(top5) on the above it would show the following results: [('Eating Out', 8), ('Subscriptions', 6), ('Groceries', 5), ('Auto and Gas', 5), ('Charity', 2)]
Now I was trying to separate the items (the count from the category) and I guess I'm messing up on that part.
The rest of the code looks like this:
categories = zip(*top5)
count = zip(*top5)
fig, ax = plt.subplots()
ax.bar(count,categories)
ax.set_title('# of Purchases by Category')
plt.show()
This is where the error is occurring. I can get something to show if I make count and categories a string but it doesn't actually plot anything and doesn't make sense.
The error shows (the name of this .py file I'm working in is FrequentExpenses.py)
Traceback (most recent call last):
File "C:\Users\budget\data\FrequentExpenses.py", line 24, in <module>
ax.bar(count,categories)
File "C:\Users\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\__init__.py", line 1447, in inner
return func(ax, *map(sanitize_sequence, args), **kwargs)
File "C:\Users\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\axes\_axes.py", line 2407, in bar
self._process_unit_info(xdata=x, ydata=height, kwargs=kwargs)
File "C:\Users\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\axes\_base.py", line 2189, in _process_unit_info
kwargs = _process_single_axis(xdata, self.xaxis, 'xunits', kwargs)
File "C:\Users\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\axes\_base.py", line 2172, in _process_single_axis
axis.update_units(data)
File "C:\Users\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\axis.py", line 1460, in update_units
converter = munits.registry.get_converter(data)
File "C:\Users\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\units.py", line 210, in get_converter
first = cbook.safe_first_element(x)
File "C:\Users\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0\LocalCache\local-packages\Python38\site-packages\matplotlib\cbook\__init__.py", line 1669, in safe_first_element
raise RuntimeError("matplotlib does not support generators "
RuntimeError: matplotlib does not support generators as input
The "Expense" import is another another file (Expense.py) which looks like this that create two classes (Expenses & Expense) and also has a method of read_expenses()
import csv
from datetime import datetime
class Expense():
def __init__(self, date_str, vendor, category, amount):
self.date_time = datetime.strptime(date_str, '%m/%d/%Y %H:%M:%S')
self.vendor = vendor
self.category = category
self.amount = amount
class Expenses():
def __init__(self):
self.list = []
self.sum = 0
# Read in the December spending data, row[2] is the $$, and need to format $$
def read_expenses(self,filename):
with open(filename, newline='') as csvfile:
csvreader = csv.reader(csvfile, delimiter=',')
for row in csvreader:
if '-' not in row[3]:
continue
amount = float((row[3][2:]).replace(',',''))
self.list.append(Expense(row[0],row[1], row[2], amount))
self.sum = amount
def categorize_for_loop(self):
necessary_expenses = set()
food_expenses = set()
unnecessary_expenses = set()
for i in self.list:
if (i.category == 'Phone' or i.category == 'Auto and Gas' or
i.category == 'Classes' or i.category == 'Utilities' or
i.category == 'Mortgage'):
necessary_expenses.add(i)
elif(i.category == 'Groceries' or i.category == 'Eating Out'):
food_expenses.add(i)
else:
unnecessary_expenses.add(i)
return [necessary_expenses, food_expenses, unnecessary_expenses]
I know this seems pretty simple to most, can anyone help me? I appreciate all the help and I'm looking forward to learning much more about python!
CodePudding user response:
Python knows a data type called “generators” which is a thing which generates values when asked (similar to an iterator). Very often it is cheaper to have a generator than to have a list produced up front. One example is that zip()
function. Instead of returning a list of tuples it returns a generator which in turn would return one tuple after the other:
zip([1,2,3],[4,5,6])
<zip object at 0x7f7955c6dd40>
If you iterate over such a generator it will generate one value after the other, so in this case it behaves like a list:
for q in zip([1,2,3],[4,5,6]):
print(q)
(1, 4)
(2, 5)
(3, 6)
But in other contexts it doesn't behave like the list, e.g. if it is being asked for the length of the result. A generator (typically) doesn't know that up front:
len(zip([1,2,3],[4,5,6]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'zip' has no len()
This is mostly to save time during execution and is called lazy evaluation. Read more about generators in general.
In your case, you can simply skip the performance optimization by constructing a true list out of the generator by calling list(...)
explicitly:
r = list(zip([1,2,3],[4,5,6]))
Then you can also ask for the length of the result:
len(r)
3
The matlib
library will probably do this internally as well, so it accepts lists as input but not generators. Pass it a list instead of a generator, and you will be fine.