mcqpy-core 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/PKG-INFO +1 -1
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/pyproject.toml +1 -1
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/config.py +23 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/overall_analysis.py +52 -2
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/helpers.py +5 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/README.md +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/_selection.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/main.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/token.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/web.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/init.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/main.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/check_tags.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/init.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/main.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/render.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/validate.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/autofill.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/check_filter.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/main.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/front_config.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/question_analysis.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/utils.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/grader.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/rubric.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/types.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/header_config.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/manifest.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/base_filter.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/date.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/difficulty.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/factory.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/manifest.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/slug.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/stratified.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/tag.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/question.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/question_bank.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/utils.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/web/__init__.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/web/bundle.py +0 -0
- {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/web/token.py +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Quiz configuration management using Pydantic and YAML."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from sys import path
|
|
4
5
|
from typing import Any, Literal
|
|
5
6
|
|
|
6
7
|
from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator
|
|
@@ -142,6 +143,28 @@ class QuizConfig(BaseModel):
|
|
|
142
143
|
raise FileNotFoundError(f"Questions path does not exist: {resolved_path}")
|
|
143
144
|
|
|
144
145
|
return data
|
|
146
|
+
|
|
147
|
+
@model_validator(mode="before")
|
|
148
|
+
def selection_path_make_absolute(cls, data):
|
|
149
|
+
"""Ensure that selection paths are relative to the config file's directory"""
|
|
150
|
+
selection = data.get("selection", None)
|
|
151
|
+
if selection is None:
|
|
152
|
+
return data
|
|
153
|
+
|
|
154
|
+
filters = selection.get("filters", None)
|
|
155
|
+
|
|
156
|
+
if filters is None:
|
|
157
|
+
return data
|
|
158
|
+
|
|
159
|
+
config_path = data.get("path")
|
|
160
|
+
for filter_name, filter_params in filters.items():
|
|
161
|
+
for name, param in filter_params.items():
|
|
162
|
+
if name.endswith("_path") and isinstance(param, str) and config_path is not None:
|
|
163
|
+
absolute_path = (Path(config_path).parent / param).resolve()
|
|
164
|
+
filter_params[name] = str(absolute_path)
|
|
165
|
+
|
|
166
|
+
return data
|
|
167
|
+
|
|
145
168
|
|
|
146
169
|
def yaml_dump(self) -> str:
|
|
147
170
|
"""Dump the current configuration to a YAML string"""
|
|
@@ -8,6 +8,8 @@ from mcqpy_core.grading.types import GradedSet
|
|
|
8
8
|
from scipy.stats import pearsonr
|
|
9
9
|
from mcqpy_core.grading.analysis.utils import AnalysisFigure
|
|
10
10
|
|
|
11
|
+
COLROMAP = plt.cm.plasma
|
|
12
|
+
|
|
11
13
|
def get_points_array(graded_sets: list[GradedSet]):
|
|
12
14
|
|
|
13
15
|
n_questions = max(len(graded_set.graded_questions) for graded_set in graded_sets)
|
|
@@ -37,11 +39,15 @@ def correlation_analysis(graded_sets: list[GradedSet], output_dir: str | Path):
|
|
|
37
39
|
correlations[qidx] = correlation
|
|
38
40
|
p_values[qidx] = p_value
|
|
39
41
|
|
|
42
|
+
|
|
43
|
+
# Colors based on correlation
|
|
44
|
+
colors = COLROMAP((correlations - np.min(correlations)) / (np.max(correlations) - np.min(correlations)))
|
|
45
|
+
|
|
40
46
|
fig, ax = plt.subplots(figsize=(8, 5))
|
|
41
47
|
ax.bar(
|
|
42
48
|
np.arange(1, n_questions + 1),
|
|
43
49
|
correlations,
|
|
44
|
-
color=
|
|
50
|
+
color=colors,
|
|
45
51
|
alpha=0.7,
|
|
46
52
|
edgecolor="black",
|
|
47
53
|
)
|
|
@@ -111,11 +117,15 @@ def discrimination_index_analysis(graded_sets: list[GradedSet], output_dir: str
|
|
|
111
117
|
D = (np.mean(q_points[high_group]) - np.mean(q_points[low_group])) / max_score
|
|
112
118
|
discrimination_indices[qidx] = D
|
|
113
119
|
|
|
120
|
+
|
|
121
|
+
# Colors based on discrimination index
|
|
122
|
+
colors = COLROMAP((discrimination_indices - np.min(discrimination_indices)) / (np.max(discrimination_indices) - np.min(discrimination_indices)))
|
|
123
|
+
|
|
114
124
|
fig, ax = plt.subplots(figsize=(8, 5))
|
|
115
125
|
ax.bar(
|
|
116
126
|
np.arange(1, n_questions + 1),
|
|
117
127
|
discrimination_indices,
|
|
118
|
-
color=
|
|
128
|
+
color=colors,
|
|
119
129
|
alpha=0.7,
|
|
120
130
|
edgecolor="black",
|
|
121
131
|
)
|
|
@@ -138,6 +148,42 @@ def discrimination_index_analysis(graded_sets: list[GradedSet], output_dir: str
|
|
|
138
148
|
)
|
|
139
149
|
|
|
140
150
|
|
|
151
|
+
def quesstion_point_average_analysis(graded_sets: list[GradedSet], output_dir: str | Path):
|
|
152
|
+
point_array = get_points_array(graded_sets)
|
|
153
|
+
|
|
154
|
+
n_questions = point_array.shape[1]
|
|
155
|
+
|
|
156
|
+
average_points = np.mean(point_array, axis=0)
|
|
157
|
+
|
|
158
|
+
# Colors based on average points
|
|
159
|
+
colors = COLROMAP(average_points / np.max(point_array))
|
|
160
|
+
|
|
161
|
+
fig, ax = plt.subplots(figsize=(8, 5))
|
|
162
|
+
ax.bar(
|
|
163
|
+
np.arange(1, n_questions + 1),
|
|
164
|
+
average_points,
|
|
165
|
+
color=colors,
|
|
166
|
+
alpha=0.7,
|
|
167
|
+
edgecolor="black",
|
|
168
|
+
)
|
|
169
|
+
plt.xlabel("Question Number")
|
|
170
|
+
plt.ylabel("Average Points")
|
|
171
|
+
plt.title("Average Points per Question")
|
|
172
|
+
|
|
173
|
+
ax.set_xticks(np.arange(1, n_questions + 1))
|
|
174
|
+
|
|
175
|
+
plt.tight_layout()
|
|
176
|
+
|
|
177
|
+
name = "quiz_question_point_average_analysis.pdf"
|
|
178
|
+
output_path = output_dir / name
|
|
179
|
+
|
|
180
|
+
plt.savefig(output_path)
|
|
181
|
+
plt.close()
|
|
182
|
+
return AnalysisFigure(
|
|
183
|
+
name=name,
|
|
184
|
+
caption=r"\textbf{Average points per question}: Shows the average points earned on each question across all students.",
|
|
185
|
+
)
|
|
186
|
+
|
|
141
187
|
def make_quiz_analysis(graded_sets: list[GradedSet], out_directory: str | Path):
|
|
142
188
|
out_directory = Path(out_directory)
|
|
143
189
|
out_directory.mkdir(parents=True, exist_ok=True)
|
|
@@ -147,10 +193,14 @@ def make_quiz_analysis(graded_sets: list[GradedSet], out_directory: str | Path):
|
|
|
147
193
|
pd_fig = point_distribution_analysis(graded_sets, out_directory)
|
|
148
194
|
figures.append(pd_fig)
|
|
149
195
|
|
|
196
|
+
avg_fig = quesstion_point_average_analysis(graded_sets, out_directory)
|
|
197
|
+
figures.append(avg_fig)
|
|
198
|
+
|
|
150
199
|
corr_fig = correlation_analysis(graded_sets, out_directory)
|
|
151
200
|
figures.append(corr_fig)
|
|
152
201
|
|
|
153
202
|
di_fig = discrimination_index_analysis(graded_sets, out_directory)
|
|
154
203
|
figures.append(di_fig)
|
|
155
204
|
|
|
205
|
+
|
|
156
206
|
return figures
|
|
@@ -13,12 +13,17 @@ def get_grade_dataframe(
|
|
|
13
13
|
|
|
14
14
|
records = []
|
|
15
15
|
for graded_set in graded_sets:
|
|
16
|
+
|
|
17
|
+
# Determine the number of questions the student gave an answer to
|
|
18
|
+
answered_questions = sum(1 in q.student_answers for q in graded_set.graded_questions)
|
|
19
|
+
|
|
16
20
|
record = {}
|
|
17
21
|
record.update(graded_set.student_info)
|
|
18
22
|
record.update(
|
|
19
23
|
{
|
|
20
24
|
"total_points": graded_set.points,
|
|
21
25
|
"max_points": graded_set.max_points,
|
|
26
|
+
"answered_questions": answered_questions,
|
|
22
27
|
}
|
|
23
28
|
)
|
|
24
29
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|