-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfinal_project.py
378 lines (317 loc) · 14 KB
/
final_project.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# File: final_project.py
# Author: Joshua Bush, Austin Byrom
# Date: 05/09/2022
# Email: [email protected], [email protected]
import csv as csv
import os as os
import numpy as np
from matplotlib import pyplot as plt
from statistics import mean as avg
from warnings import filterwarnings as fw
from typing import TypedDict
class StudentDict(TypedDict):
"""StudentDict dictionary definition to enforce data types used as input when creating an instance of `Student`.
Descriptions of the attribute types can be found in the attributes of `Student`"""
uin: str
labs: list[float]
quizzes: list[float]
readings: list[float]
exams: list[float]
project: float
class Student:
"""A class that represents a student in a classroom."""
def __init__(self, student_data: StudentDict):
#: Student UIN represented as a string
self.uin = student_data["uin"]
#: List containing floats of the student's lab grades
self.labs = student_data["labs"]
#: List containing floats of the student's quiz grades
self.quizzes = student_data["quizzes"]
#: List containing floats of the student's reading grades
self.readings = student_data["readings"]
#: List containing floats of the student's exam grades
self.exams = student_data["exams"]
#: Float containing the student's project grade
self.project = student_data["project"]
#: Float containing the student's total generated by `Student.analyze` using `ClassSet` weights
self.total = None
#: String containing the total grade of the Student using the total generated by `Student.analyze`
self.letter = None
def analyze(self, class_d: "ClassSet", to_print: bool) -> None:
"""Analyzes the student grades and generates a corresponding `Student.total` and `Student.letter` grade.
If toggled via bool, the function will also print the student statistics to a .txt file with their
`Student.uin` as the filename. Requires the passing of a `ClassSet` instance to determine the weights
assigned to each category. Assumes standard A-F grading scheme for assigning `Student.letter`.
Args:
class_d:
The instance of `ClassSet` the `Student` is stored in, to extract assignment weights.
to_print:
A bool value on whether to generate the associated .txt file when the method is called.
Returns:
None
"""
means = [avg(self.exams), avg(self.labs), avg(self.quizzes), avg(self.readings), self.project]
self.total = sum([m * w for m, w in zip(means, class_d.weights)])
if self.total >= 90:
let = 'A'
elif self.total >= 80:
let = 'B'
elif self.total >= 70:
let = 'C'
elif self.total >= 60:
let = 'D'
else:
let = 'F'
self.letter = let
if to_print:
with open(f'{self.uin}.txt', 'w') as file_report:
file_report.write(f"""Exams mean: {means[0]:.1f}
Labs mean: {means[1]:.1f}
Quizzes mean: {means[2]:.1f}
Reading activities mean: {means[3]:.1f}
Score: {self.total:.1f}%
Letter grade: {let}
""")
@staticmethod
def cast_student_dict(student: list) -> StudentDict or None:
"""Static method of `Student` that is used to validate input data before creating an instance.
This method attempts to cast input data read from a csv file to various variables with strict type definitions.
If there is an error in the type casting the function will print an error message and return None, otherwise
it will return an instance of a `StudentDict`.
Args:
student: A list containing all the values of the csv row passed to the method.
Returns:
A `StudentDict` object to be passed into a new instance of `Student`. If the data is not valid in
accordance the `StudentDict` attribute definitions, None will be returned instead.
Raises:
ValueError: Raises ValueError when there is an issue in casting values to string or float,
indicates invalid student data.
"""
try:
#: String cast of what should be the `Student` uin
uin = str(student[0])
#: List of floats containing lab grades (csv column 2-7)
labs = [float(_) for _ in student[1:7]]
#: List of floats containing quiz grades (csv column 8-13)
quizzes = [float(_) for _ in student[7:13]]
#: List of floats containing reading grades (csv column 14-19)
readings = [float(_) for _ in student[13:19]]
#: List of floats containing exam grades (csv column 20-22)
exams = [float(_) for _ in student[19:22]]
#: Float of project grade (csv column 23)
project = float(student[22])
except ValueError:
print(f'Invalid student in file')
return None
return StudentDict(uin=uin,
labs=labs,
quizzes=quizzes,
readings=readings,
exams=exams,
project=project)
def student_graphs(self):
"""Generates various graphs of the student's grades using pyPlot from matplotlib."""
try:
os.mkdir(f'{self.uin}')
except FileExistsError:
print(f'Student already has graphs printed at folder: "{self.uin}"')
return
for attr, val in self.__dict__.items():
if attr in ['uin', 'project', 'total', 'letter']:
continue
attr = str(attr)
attr = attr.capitalize()
x = np.arange(len(val))
for i in range(len(x)):
x[i] += 1
width = 0.5
fig, ax = plt.subplots()
ax.set_xticks(x)
ax.set_title(attr)
ax.set_ylabel("Score")
if attr == 'Quizzes':
ax.set_xlabel(f'Quiz')
else:
ax.set_xlabel(f'{attr[0:-1]}')
data = ax.bar(x, val, width, label=attr)
for i in data:
height = i.get_height()
ax.annotate('{}'.format(height),
xy=(i.get_x() + i.get_width() / 2, height),
xytext=(0, 2),
textcoords="offset points",
ha='center', va='bottom')
plt.savefig(f"{self.uin}/{attr}_graph.png")
class ClassSet:
"""A class representing a CSCE 110 class, initialized with a list of floats containing the
various assignment weights"""
def __init__(self, weights: tuple[float, float, float, float, float]):
#: A list containing `Student` objects, initialized via the `ClassSet.populate_class` method
self.students = None
#: An integer containing the number of students in the instance (the length of `ClassSet.students`)
self.num_students = None
#: A tuple of floats containing the weights of assignment categories to be used in `Student.analyze`
self.weights = weights
def populate_class(self) -> None:
"""Populates an instance of `ClassSet` with `Student` instances from a csv file.
Prompts user for a filepath containing a .csv extension and attempts to read a csv file at that filepath.
If the csv file is valid, it will validate each row as being a valid student using `Student.cast_student_dict`
and append that to the `ClassSet.students` attribute (a list containing `Student` objects)."""
file_path = str(input('Enter file path: '))
students = []
with open(file_path, newline='') as class_file:
student_list = csv.reader(class_file, delimiter=',')
for index, row in enumerate(student_list):
if index == 0:
continue
data = Student.cast_student_dict(row)
if not data:
continue
else:
students.append(Student(data))
self.students = students
self.num_students = len(self.students)
def find_student(self) -> "Student":
"""Searches for a `Student` instance where `Student.uin` is the same as a UIN of a user input.
In addition, the function will validate the user input as a proper UIN (containing all digits and having
a length of 10.)"""
search_uin = str(input('Enter student uin: '))
if not (search_uin.isnumeric() and len(search_uin) == 10):
print('Invalid UIN, please try again...')
return self.find_student()
else:
for student in self.students:
if student.uin == search_uin:
return student
else:
print('Invalid UIN, please try again...')
return self.find_student()
def class_analysis(self):
"""Analyzes the `Student.total` of each student in a `ClassSet` instance and provides stats in a .txt file."""
for student in self.students:
if not student.total:
student.analyze(self, False)
class_grades = [student.total for student in self.students]
grades_list = f"""Total number of students: {self.num_students}
Minimum score: {min(class_grades):.2f}
Maximum score: {max(class_grades):.2f}
Median score: {np.median(class_grades):.2f}
Mean score: {avg(class_grades):.2f}
Standard deviation: {np.std(class_grades):.2f}"""
with open('report.txt', 'w') as class_report:
class_report.write(grades_list)
def class_graphs(self):
"""Generates various graphs of the class's grades using pyPlot from matplotlib."""
let_list = []
for student in self.students:
if not student.letter:
student.analyze(self, False)
let_list.append(student.letter)
final_grade_counts = [let_list.count('A'), let_list.count('B'),
let_list.count('C'),
let_list.count('D'), let_list.count('F')]
x = np.arange(len(final_grade_counts))
width = 0.5
fig, ax = plt.subplots()
ax.set_title("Class Letter Grades")
ax.set_ylabel("Number of Students")
ax.set_xlabel("Letter Grade")
a = ax.get_xticks().tolist()
a[1] = 'A'
a[2] = 'B'
a[3] = 'C'
a[4] = 'D'
a[5] = 'F'
ax.set_xticklabels(a)
data = ax.bar(x, final_grade_counts, width)
counter = 0
for i in data:
y_pos = i.get_height()
ax.annotate(str(f'{final_grade_counts[counter]}').format(y_pos),
xy=(i.get_x() + i.get_width() / 2, y_pos),
xytext=(0, 5),
textcoords="offset points",
ha='center', va='bottom')
counter += 1
plt.savefig('class_charts/class_graphs.png')
def class_pie(self):
"""Generates pie graph of the all instances of `Student.total` in the instance using pyPlot from matplotlib."""
let_list = []
for student in self.students:
if not student.letter:
student.analyze(self, False)
else:
let_list.append(student.letter)
final_grade_counts = [let_list.count('A'), let_list.count('B'),
let_list.count('C'),
let_list.count('D'), let_list.count('F')]
y = np.array(final_grade_counts)
labels = ["A", "B", "C", "D", "F"]
plt.pie(y, labels=labels, autopct='%.2f%%')
plt.title('Class Letter Grades')
plt.savefig('class_charts/class_pie_chart.png')
def menu() -> int:
"""Prints out the menu and prompts user for an option to be passed back to `main`.
Returns:
An integer representing the choice made by the user.
Raises:
ValueError: Raises value error when the user input cannot be cast to int, signaling an invalid input.
"""
print("""
*******************Main Menu*****************
1. Read CSV file of grades
2. Generate student report file
3. Generate student report charts
4. Generate class report file
5. Generate class report charts
6. Quit
************************************************\n""")
try:
opt = str(input('Enter option: '))
if opt.lower() == 'q' or opt.lower() == 'quit':
return 6
else:
return int(opt)
except ValueError:
print('Invalid input \n')
return menu()
def main() -> None:
"""The main driver of the python file, the heart of the cannoli.
Calls the `menu` function, and using the int return, will call the corresponding function(s). Loops until
user quits the program or the class instance is no longer available.
"""
#: Initialized instance of `ClassSet`, all other functions requiring a `ClassSet` instance will use this.
csce_class = ClassSet((0.45, 0.25, 0.10, 0.10, 0.10))
while csce_class:
choice = menu()
match choice:
case 1:
csce_class.populate_class()
continue
case 2:
analysis_student = csce_class.find_student()
analysis_student.analyze(csce_class, True)
continue
case 3:
chart_student = csce_class.find_student()
chart_student.student_graphs()
continue
case 4:
csce_class.class_analysis()
case 5:
try:
os.mkdir('class_charts')
except FileExistsError:
print('Class has already been analyzed!')
continue
csce_class.class_graphs()
csce_class.class_pie()
case 6:
return
case _:
print('Invalid input \n')
continue
# Call the main function
if __name__ == "__main__":
fw("ignore")
main()