Skip to content

Commit c158706

Browse files
authored
Merge pull request #46 from iansan5653/feature/mcta-support
Add support for outputting files for MCTA
2 parents c348600 + a369244 commit c158706

File tree

5 files changed

+123
-3
lines changed

5 files changed

+123
-3
lines changed

code/data_exporting.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def validate_order_map(order_map: tp.Dict[str, tp.List[int]],
4343
f"Arrangement file entry for '{form_code}' is invalid. All arrangement file entries must contain one of each index from 1 to the number of questions."
4444
)
4545

46+
def save_csv(data: tp.List[tp.List[str]], path: pathlib.PurePath):
47+
with open(path, "w", newline="") as f:
48+
writer = csv.writer(f)
49+
writer.writerows(data)
4650

4751
class OutputSheet():
4852
"""A lightweight matrix of data to be exported. Faster than a dataframe but
@@ -53,13 +57,15 @@ class OutputSheet():
5357
num_questions: int
5458
row_count: int
5559
first_question_column_index: int
60+
form_code_column_index: int
5661

5762
def __init__(self, columns: tp.List[RealOrVirtualField], num_questions: int):
5863
self.field_columns = columns
5964
self.num_questions = num_questions
6065
field_column_names = [COLUMN_NAMES[column] for column in columns]
6166
answer_columns = [f"Q{i + 1}" for i in range(self.num_questions)]
6267
self.first_question_column_index = len(field_column_names)
68+
self.form_code_column_index = self.field_columns.index(Field.TEST_FORM_CODE)
6369
self.data = [field_column_names + answer_columns]
6470
self.row_count = 0
6571

@@ -71,9 +77,7 @@ def save(self, path: pathlib.PurePath, filebasename: str, sort: bool,
7177
data = self.data
7278
if(transpose):
7379
data = list_utils.transpose(data)
74-
with open(str(output_path), 'w+', newline='') as output_file:
75-
writer = csv.writer(output_file)
76-
writer.writerows(data)
80+
save_csv(data, output_path)
7781
return output_path
7882

7983
def delete_field_column(self, column: RealOrVirtualField):

code/main.py

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import grid_info as grid_i
99
import grid_reading as grid_r
1010
import image_utils
11+
from mcta_processing import transform_and_save_mcta_output
1112
import scoring
1213
import user_interface
1314

@@ -21,6 +22,7 @@
2122
keys_file = user_input.keys_file
2223
arrangement_file = user_input.arrangement_map
2324
sort_results = user_input.sort_results
25+
output_mcta = user_input.output_mcta
2426
debug_mode_on = user_input.debug_mode
2527
form_variant = grid_i.form_150q if user_input.form_variant == user_interface.FormVariantSelection.VARIANT_150_Q else grid_i.form_75q
2628

@@ -105,6 +107,7 @@
105107
form_code_field, grid, threshold, form_variant,
106108
field_fill_percents[form_code_field]) or ""
107109
keys_results.add(field_data, answers)
110+
108111
else:
109112
for field in form_variant.fields.keys():
110113
field_value = grid_r.read_field_as_string(
@@ -146,6 +149,7 @@
146149
sort_results,
147150
timestamp=files_timestamp,
148151
transpose=True)
152+
149153
success_string += "✔️ Key processed and saved.\n"
150154

151155
scores = scoring.score_results(answers_results, keys_results,
@@ -171,6 +175,9 @@
171175
timestamp=files_timestamp)
172176
success_string += "✔️ All scored results processed and saved."
173177

178+
if (output_mcta):
179+
transform_and_save_mcta_output(answers_results, keys_results, files_timestamp, output_folder)
180+
174181
progress.set_status(success_string, False)
175182
except (RuntimeError, ValueError) as e:
176183
wrapped_err = "\n".join(textwrap.wrap(str(e), 70))

code/mcta_processing.py

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import typing as tp
2+
import pathlib
3+
import datetime
4+
import itertools
5+
6+
from data_exporting import format_timestamp_for_file, save_csv, OutputSheet
7+
8+
"""Support for additional outputs used by the Multiple Choice Test Analysis software."""
9+
10+
def transform_and_save_mcta_output(answers_results: OutputSheet,
11+
keys_results: OutputSheet,
12+
files_timestamp: datetime,
13+
output_folder: pathlib.Path):
14+
"""Generate and save files that are specific to a downstream Multiple Choice Test Analysis
15+
software. The format of these files is dependend on the downstream software, so they are not
16+
consistent with the rest of the output."""
17+
create_keys_files(keys_results, output_folder, files_timestamp)
18+
create_answers_files(answers_results, output_folder, files_timestamp)
19+
20+
21+
def create_keys_files(keys_results: OutputSheet, output_folder: pathlib.Path, files_timestamp: datetime):
22+
"""Create the key files for the Multiple Choice Test Analysis software.
23+
24+
Params:
25+
keys_results: The results of the keys file.
26+
output_folder: The folder to save the files to.
27+
files_timestamp: The timestamp to use for the files.
28+
"""
29+
form_code_col = keys_results.form_code_column_index
30+
31+
for row in keys_results.data[1:]:
32+
code = row[form_code_col]
33+
csv_data = build_key_csv(row[keys_results.first_question_column_index:])
34+
save_mcta_csv(csv_data, output_folder, f"{code}_key", files_timestamp)
35+
36+
37+
def create_answers_files(answers_results: OutputSheet,
38+
output_folder: pathlib.Path,
39+
files_timestamp: datetime):
40+
"""Create the answer files for the Multiple Choice Test Analysis software.
41+
42+
Params:
43+
answers_results: The results of the answers file.
44+
output_folder: The folder to save the files to.
45+
files_timestamp: The timestamp to use for the files.
46+
"""
47+
form_code_col = answers_results.form_code_column_index
48+
first_question_col = answers_results.first_question_column_index
49+
50+
# Preserve the original index for naming students anonymously
51+
# List of tuples of (form code, original index, answers)
52+
answers_with_form_code = [(row[form_code_col], i, row[first_question_col:]) for (i, row) in enumerate(answers_results.data[1:])]
53+
54+
# groupby requires sorted input
55+
sorted_by_code = sorted(answers_with_form_code, key=lambda x: x[0])
56+
grouped_by_code = itertools.groupby(sorted_by_code, key=lambda x: x[0])
57+
58+
# Generate one output file for each form code in the answers data
59+
for code, group in grouped_by_code:
60+
group_data = [(original_index, answers) for (_, original_index, answers) in group]
61+
csv_data = build_answers_csv(group_data)
62+
# Test form code can be in [A|B] form if student selects A and B. The [|] are not safe for filename.
63+
file_safe_code = code.replace("[", "").replace("]", "").replace("|", "")
64+
save_mcta_csv(csv_data, output_folder, f"{file_safe_code}_results", files_timestamp)
65+
66+
67+
def build_key_csv(answers: tp.List[str]) -> tp.List[tp.List[str]]:
68+
"""Build the CSV data for a key file. Each key outputs a separate pair of key and answer files.
69+
70+
Params:
71+
answers: All of the answers for this form code, in order.
72+
"""
73+
header = ["", "Answer", "Title", "Concept"]
74+
data = [[f"Q{i}", x, f"Q{i}", "unknown"] for i, x in enumerate(answers, 1)]
75+
return [header] + data
76+
77+
78+
def build_answers_csv(data: tp.List[tp.Tuple[int, tp.List[str]]]) -> tp.List[tp.List[str]]:
79+
"""Build the CSV data for an answers file. Should be called once for each form code.
80+
81+
Params:
82+
data: The data to save into the file. A list of rows, where each row represents a student.
83+
Each row is a tuple of the student's original index (for naming) and the list of
84+
answers.
85+
"""
86+
header = [""] + [f"Q{i + 1}" for i in range(0, len(data[0][1]))]
87+
rows = [[f"Student{i}"] + answers for (i, answers) in data]
88+
return [header] + rows
89+
90+
91+
def save_mcta_csv(data: tp.List[tp.List[str]],
92+
path: pathlib.PurePath,
93+
basefilename: str,
94+
timestamp: datetime):
95+
filename = path / f"{format_timestamp_for_file(timestamp)}__mcta_{basefilename}.csv"
96+
save_csv(data, filename)

code/user_interface.py

+13
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ def disable(self):
283283
class OutputFolderPickerWidget():
284284
folder: tp.Optional[Path]
285285
sort_results: bool
286+
output_mcta: bool
286287
sort_toggle_count: int
287288

288289
def __init__(self,
@@ -301,11 +302,15 @@ def __init__(self,
301302
self.__sort_results_checkbox = CheckboxWidget(
302303
container, "Sort results by students' names.",
303304
self.__on_sort_update)
305+
self.__output_mcta_checkbox = CheckboxWidget(
306+
container, "Output additional files for MCTA.",
307+
self.__on_update, reduce_padding_above=True)
304308

305309
pack(container, fill=tk.X)
306310

307311
self.folder = None
308312
self.sort_results = False
313+
self.output_mcta = False
309314
self.sort_toggle_count = 0
310315

311316
def __on_sort_update(self):
@@ -315,13 +320,15 @@ def __on_sort_update(self):
315320
def __on_update(self):
316321
self.folder = self.__output_folder_picker.value
317322
self.sort_results = self.__sort_results_checkbox.value
323+
self.output_mcta = self.__output_mcta_checkbox.value
318324

319325
if self.__on_change is not None:
320326
self.__on_change()
321327

322328
def disable(self):
323329
self.__output_folder_picker.disable()
324330
self.__sort_results_checkbox.disable()
331+
self.__output_mcta_checkbox.disable()
325332

326333

327334
class AnswerKeyPickerWidget():
@@ -443,6 +450,7 @@ class MainWindow:
443450
keys_file: tp.Optional[Path]
444451
arrangement_map: tp.Optional[Path]
445452
sort_results: bool
453+
output_mcta: bool
446454
debug_mode: bool = False
447455
form_variant: FormVariantSelection
448456

@@ -566,6 +574,11 @@ def __on_update(self):
566574
else:
567575
new_status += f"Input sort order will be maintained.\n"
568576

577+
578+
self.output_mcta = self.__output_folder_picker.output_mcta
579+
if self.output_mcta:
580+
new_status += "Additional files will be output for use with analysis software."
581+
569582
if self.__output_folder_picker.sort_toggle_count > 15:
570583
new_status += "WARNING: Debug mode enabled. Restart to disable."
571584
self.debug_mode = True

examples/batch-B/11.jpg

-1.49 MB
Loading

0 commit comments

Comments
 (0)