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.
Files changed (51) hide show
  1. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/PKG-INFO +1 -1
  2. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/pyproject.toml +1 -1
  3. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/config.py +23 -0
  4. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/overall_analysis.py +52 -2
  5. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/helpers.py +5 -0
  6. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/README.md +0 -0
  7. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/__init__.py +0 -0
  8. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/__init__.py +0 -0
  9. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/_selection.py +0 -0
  10. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/__init__.py +0 -0
  11. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/main.py +0 -0
  12. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/token.py +0 -0
  13. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/export/web.py +0 -0
  14. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/init.py +0 -0
  15. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/main.py +0 -0
  16. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/__init__.py +0 -0
  17. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/check_tags.py +0 -0
  18. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/init.py +0 -0
  19. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/main.py +0 -0
  20. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/render.py +0 -0
  21. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/question/validate.py +0 -0
  22. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/__init__.py +0 -0
  23. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/autofill.py +0 -0
  24. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/check_filter.py +0 -0
  25. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/cli/utils/main.py +0 -0
  26. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/front_config.py +0 -0
  27. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/__init__.py +0 -0
  28. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/__init__.py +0 -0
  29. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/question_analysis.py +0 -0
  30. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/analysis/utils.py +0 -0
  31. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/grader.py +0 -0
  32. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/rubric.py +0 -0
  33. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/grading/types.py +0 -0
  34. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/header_config.py +0 -0
  35. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/manifest.py +0 -0
  36. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/__init__.py +0 -0
  37. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/__init__.py +0 -0
  38. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/base_filter.py +0 -0
  39. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/date.py +0 -0
  40. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/difficulty.py +0 -0
  41. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/factory.py +0 -0
  42. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/manifest.py +0 -0
  43. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/slug.py +0 -0
  44. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/stratified.py +0 -0
  45. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/filter/tag.py +0 -0
  46. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/question.py +0 -0
  47. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/question_bank.py +0 -0
  48. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/question/utils.py +0 -0
  49. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/web/__init__.py +0 -0
  50. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/web/bundle.py +0 -0
  51. {mcqpy_core-0.2.3 → mcqpy_core-0.2.4}/src/mcqpy_core/web/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: mcqpy-core
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Core question, manifest, grading, analysis, and browser-safe runtime logic for MCQPy
5
5
  Author: Mads-Peter
6
6
  Author-email: Mads-Peter <machri@phys.au.dk>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcqpy-core"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Core question, manifest, grading, analysis, and browser-safe runtime logic for MCQPy"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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="mediumpurple",
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="mediumpurple",
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