adaptivetesting 1.1.5__tar.gz → 1.2b1__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 (138) hide show
  1. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/PKG-INFO +14 -15
  2. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/README.md +2 -5
  3. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/__init__.py +5 -2
  4. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/data/__csv_context.py +23 -10
  5. adaptivetesting-1.2b1/adaptivetesting/data/__read_prev_items_exp_cont.py +35 -0
  6. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/implementations/__pre_test.py +10 -3
  7. adaptivetesting-1.2b1/adaptivetesting/implementations/__test_assembler.py +418 -0
  8. adaptivetesting-1.2b1/adaptivetesting/math/__gen_response_pattern.py +117 -0
  9. adaptivetesting-1.2b1/adaptivetesting/math/content_balancing/__constraint.py +23 -0
  10. adaptivetesting-1.2b1/adaptivetesting/math/content_balancing/__content_balancing.py +35 -0
  11. adaptivetesting-1.2b1/adaptivetesting/math/content_balancing/__functions.py +265 -0
  12. adaptivetesting-1.2b1/adaptivetesting/math/content_balancing/__init__.py +1 -0
  13. adaptivetesting-1.2b1/adaptivetesting/math/content_balancing/__maximum_priority_index.py +82 -0
  14. adaptivetesting-1.2b1/adaptivetesting/math/content_balancing/__weighted_penalty_model.py +282 -0
  15. adaptivetesting-1.2b1/adaptivetesting/math/estimators/__bayes_modal_estimation.py +122 -0
  16. adaptivetesting-1.2b1/adaptivetesting/math/estimators/__expect_a_posteriori.py +164 -0
  17. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/math/estimators/__init__.py +1 -1
  18. adaptivetesting-1.2b1/adaptivetesting/math/estimators/__ml_estimation.py +109 -0
  19. adaptivetesting-1.2b1/adaptivetesting/math/estimators/__prior.py +183 -0
  20. adaptivetesting-1.2b1/adaptivetesting/math/estimators/__test_information.py +181 -0
  21. adaptivetesting-1.2b1/adaptivetesting/math/exposure_control/__exposure_control.py +32 -0
  22. adaptivetesting-1.2b1/adaptivetesting/math/exposure_control/__init__.py +2 -0
  23. adaptivetesting-1.2b1/adaptivetesting/math/exposure_control/__mpi_exposure_control.py +81 -0
  24. adaptivetesting-1.2b1/adaptivetesting/math/exposure_control/__randomesque.py +114 -0
  25. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/math/item_selection/__maximum_information_criterion.py +7 -13
  26. adaptivetesting-1.2b1/adaptivetesting/math/item_selection/__urrys_rule.py +38 -0
  27. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/models/__adaptive_test.py +2 -33
  28. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/models/__item_pool.py +87 -69
  29. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/models/__item_selection_exception.py +1 -0
  30. adaptivetesting-1.2b1/adaptivetesting/models/__test_item.py +81 -0
  31. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/services/__estimator_interface.py +16 -10
  32. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/services/__item_selection_protocol.py +0 -2
  33. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/services/__test_results_interface.py +0 -2
  34. adaptivetesting-1.2b1/adaptivetesting/simulation/__init__.py +5 -0
  35. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/simulation/__simulation.py +6 -12
  36. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_adaptive_test.py +0 -4
  37. adaptivetesting-1.2b1/adaptivetesting/tests/test_bayes_modal.py +194 -0
  38. adaptivetesting-1.2b1/adaptivetesting/tests/test_content_balacing.py +131 -0
  39. adaptivetesting-1.2b1/adaptivetesting/tests/test_eap.py +159 -0
  40. adaptivetesting-1.2b1/adaptivetesting/tests/test_exposure_control_mpi.py +87 -0
  41. adaptivetesting-1.2b1/adaptivetesting/tests/test_full_tests.py +224 -0
  42. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_generate_response_pattern.py +33 -2
  43. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_item_selection.py +12 -2
  44. adaptivetesting-1.2b1/adaptivetesting/tests/test_load_test_items.py +325 -0
  45. adaptivetesting-1.2b1/adaptivetesting/tests/test_poly_models.py +103 -0
  46. adaptivetesting-1.2b1/adaptivetesting/tests/test_priors.py +56 -0
  47. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_simulation.py +63 -1
  48. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_test_assembler.py +8 -8
  49. adaptivetesting-1.2b1/adaptivetesting/tests/test_weighted_penalty_model.py +530 -0
  50. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/utils/__plots.py +13 -11
  51. adaptivetesting-1.2b1/adaptivetesting.egg-info/PKG-INFO +374 -0
  52. adaptivetesting-1.2b1/adaptivetesting.egg-info/SOURCES.txt +80 -0
  53. adaptivetesting-1.2b1/adaptivetesting.egg-info/dependency_links.txt +1 -0
  54. adaptivetesting-1.2b1/adaptivetesting.egg-info/requires.txt +6 -0
  55. adaptivetesting-1.2b1/adaptivetesting.egg-info/top_level.txt +1 -0
  56. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/pyproject.toml +22 -12
  57. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/setup.cfg +7 -2
  58. adaptivetesting-1.2b1/setup.py +21 -0
  59. adaptivetesting-1.1.5/.github/CONTRIBUTING.md +0 -11
  60. adaptivetesting-1.1.5/.github/workflows/publish.yml +0 -39
  61. adaptivetesting-1.1.5/.github/workflows/python-test.yml +0 -60
  62. adaptivetesting-1.1.5/.gitignore +0 -170
  63. adaptivetesting-1.1.5/.gitlab-ci.yml +0 -13
  64. adaptivetesting-1.1.5/.python-version +0 -1
  65. adaptivetesting-1.1.5/.readthedocs.yml +0 -22
  66. adaptivetesting-1.1.5/.vscode/extensions.json +0 -8
  67. adaptivetesting-1.1.5/.vscode/settings.json +0 -14
  68. adaptivetesting-1.1.5/CHANGELOG.md +0 -13
  69. adaptivetesting-1.1.5/adaptivetesting/implementations/__test_assembler.py +0 -231
  70. adaptivetesting-1.1.5/adaptivetesting/math/__gen_response_pattern.py +0 -50
  71. adaptivetesting-1.1.5/adaptivetesting/math/estimators/__bayes_modal_estimation.py +0 -121
  72. adaptivetesting-1.1.5/adaptivetesting/math/estimators/__expect_a_posteriori.py +0 -90
  73. adaptivetesting-1.1.5/adaptivetesting/math/estimators/__functions/__bayes.py +0 -56
  74. adaptivetesting-1.1.5/adaptivetesting/math/estimators/__functions/__estimators.py +0 -167
  75. adaptivetesting-1.1.5/adaptivetesting/math/estimators/__ml_estimation.py +0 -65
  76. adaptivetesting-1.1.5/adaptivetesting/math/estimators/__prior.py +0 -94
  77. adaptivetesting-1.1.5/adaptivetesting/math/estimators/__test_information.py +0 -91
  78. adaptivetesting-1.1.5/adaptivetesting/math/item_selection/__urrys_rule.py +0 -33
  79. adaptivetesting-1.1.5/adaptivetesting/models/__test_item.py +0 -33
  80. adaptivetesting-1.1.5/adaptivetesting/simulation/__init__.py +0 -1
  81. adaptivetesting-1.1.5/adaptivetesting/tests/test_bayes_modal.py +0 -89
  82. adaptivetesting-1.1.5/adaptivetesting/tests/test_eap.py +0 -27
  83. adaptivetesting-1.1.5/adaptivetesting/tests/test_load_test_items.py +0 -176
  84. adaptivetesting-1.1.5/docs/Makefile +0 -20
  85. adaptivetesting-1.1.5/docs/data.md +0 -69
  86. adaptivetesting-1.1.5/docs/implementations.md +0 -117
  87. adaptivetesting-1.1.5/docs/index.md +0 -32
  88. adaptivetesting-1.1.5/docs/make.bat +0 -35
  89. adaptivetesting-1.1.5/docs/math.md +0 -254
  90. adaptivetesting-1.1.5/docs/models.md +0 -248
  91. adaptivetesting-1.1.5/docs/readme.md +0 -32
  92. adaptivetesting-1.1.5/docs/requirements.txt +0 -0
  93. adaptivetesting-1.1.5/docs/services.md +0 -42
  94. adaptivetesting-1.1.5/docs/simulation.md +0 -53
  95. adaptivetesting-1.1.5/docs/source/_static/SPEC-0.svg +0 -1
  96. adaptivetesting-1.1.5/docs/source/_static/logo.svg +0 -67
  97. adaptivetesting-1.1.5/docs/source/_static/main.css +0 -24
  98. adaptivetesting-1.1.5/docs/source/conf.py +0 -49
  99. adaptivetesting-1.1.5/docs/source/data.rst +0 -9
  100. adaptivetesting-1.1.5/docs/source/implementations.rst +0 -8
  101. adaptivetesting-1.1.5/docs/source/index.rst +0 -37
  102. adaptivetesting-1.1.5/docs/source/math.rst +0 -21
  103. adaptivetesting-1.1.5/docs/source/models.rst +0 -9
  104. adaptivetesting-1.1.5/docs/source/services.rst +0 -8
  105. adaptivetesting-1.1.5/docs/source/simulation.rst +0 -9
  106. adaptivetesting-1.1.5/docs/source/tests.rst +0 -5
  107. adaptivetesting-1.1.5/docs/tests.md +0 -4
  108. adaptivetesting-1.1.5/examples/.gitignore +0 -3
  109. adaptivetesting-1.1.5/examples/simulation.py +0 -134
  110. adaptivetesting-1.1.5/examples/testing.py +0 -108
  111. adaptivetesting-1.1.5/images/default.svg +0 -208
  112. adaptivetesting-1.1.5/images/semi-adaptive.svg +0 -254
  113. adaptivetesting-1.1.5/mypy.ini +0 -8
  114. adaptivetesting-1.1.5/requirements.txt +0 -0
  115. adaptivetesting-1.1.5/uv.lock +0 -4274
  116. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/LICENSE +0 -0
  117. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/data/__init__.py +0 -0
  118. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/data/__pickle_context.py +0 -0
  119. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/implementations/__default_implementation.py +0 -0
  120. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/implementations/__init__.py +0 -0
  121. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/implementations/__semi_implementation.py +0 -0
  122. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/math/__init__.py +0 -0
  123. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/math/item_selection/__init__.py +0 -0
  124. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/models/__algorithm_exception.py +0 -0
  125. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/models/__init__.py +0 -0
  126. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/models/__misc.py +0 -0
  127. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/models/__test_result.py +0 -0
  128. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/services/__init__.py +0 -0
  129. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/__init__.py +0 -0
  130. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_descriptives.py +0 -0
  131. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_information_test.py +0 -0
  132. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_mle.py +0 -0
  133. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_plots.py +0 -0
  134. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_pretest.py +0 -0
  135. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/tests/test_standard_error.py +0 -0
  136. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/utils/__descriptives.py +0 -0
  137. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/utils/__funcs.py +0 -0
  138. {adaptivetesting-1.1.5 → adaptivetesting-1.2b1}/adaptivetesting/utils/__init__.py +0 -0
@@ -1,28 +1,27 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: adaptivetesting
3
- Version: 1.1.5
3
+ Version: 1.2b1
4
4
  Summary: adaptivetesting is a Python package that can be used to simulate and evaluate custom CAT scenarios as well as implement them in real-world testing scenarios from a single codebase
5
+ Author-email: Jonas Engicht <dev@condecon.de>
5
6
  Project-URL: Homepage, https://github.com/condecon/adaptivetesting
6
- Project-URL: Documentation, https://github.com/condecon/adaptivetesting/tree/main/docs
7
+ Project-URL: Documentation, https://github.com/condecon/adaptivetesting/wiki
7
8
  Project-URL: Repository, https://github.com/condecon/adaptivetesting.git
8
9
  Project-URL: Issues, https://github.com/condecon/adaptivetesting/issues
9
- Author-email: Jonas Engicht <dev@condecon.de>
10
- License-File: LICENSE
11
- Keywords: computerized-adaptive-testing,item-response-theory,psychology,statistics
10
+ Keywords: statistics,psychology,item-response-theory,computerized-adaptive-testing
12
11
  Requires-Python: >=3.12
13
- Requires-Dist: matplotlib>=3.9.0
14
- Requires-Dist: numpy>=2.0.0
15
- Requires-Dist: pandas>=2.2.0
16
- Requires-Dist: scipy>=1.12.0
17
- Requires-Dist: tqdm>=4.66.0
18
12
  Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy>=2.1.0
15
+ Requires-Dist: pandas>=2.3.0
16
+ Requires-Dist: scipy>=1.15.0
17
+ Requires-Dist: tqdm>=4.66.0
18
+ Requires-Dist: matplotlib>=3.10.0
19
+ Requires-Dist: numdifftools>=0.9.42
20
+ Dynamic: license-file
19
21
 
20
22
  # adaptivetesting
21
- [![SPEC 0 — Minimum Supported Dependencies](docs/source/_static/SPEC-0.svg)](https://scientific-python.org/specs/spec-0000/)
22
-
23
-
24
23
 
25
- <img src="/docs/source/_static/logo.svg" style="width: 100%">
24
+ <img src="/docs/_static/logo.svg" style="width: 100%">
26
25
  </img>
27
26
 
28
27
  **An open-source Python package for simplified, customizable Computerized Adaptive Testing (CAT) using Bayesian methods.**
@@ -37,7 +36,7 @@ Description-Content-Type: text/markdown
37
36
  - Maximum Likelihood Estimation (MLE)
38
37
  - Bayesian Modal Estimation (BM)
39
38
  - Expected A Posteriori (EAP)
40
- - **Item Selection Strategies**: Kaximum information criterion and Urry's rule
39
+ - **Item Selection Strategies**: Maximum information criterion and Urry's rule
41
40
  - **Simulation Framework**: Comprehensive tools for CAT simulation and evaluation
42
41
  - **Real-world Application**: Direct transition from simulation to production testing
43
42
  - **Stopping Criteria**: Support for standard error and test length criteria
@@ -1,9 +1,6 @@
1
1
  # adaptivetesting
2
- [![SPEC 0 — Minimum Supported Dependencies](docs/source/_static/SPEC-0.svg)](https://scientific-python.org/specs/spec-0000/)
3
2
 
4
-
5
-
6
- <img src="/docs/source/_static/logo.svg" style="width: 100%">
3
+ <img src="/docs/_static/logo.svg" style="width: 100%">
7
4
  </img>
8
5
 
9
6
  **An open-source Python package for simplified, customizable Computerized Adaptive Testing (CAT) using Bayesian methods.**
@@ -18,7 +15,7 @@
18
15
  - Maximum Likelihood Estimation (MLE)
19
16
  - Bayesian Modal Estimation (BM)
20
17
  - Expected A Posteriori (EAP)
21
- - **Item Selection Strategies**: Kaximum information criterion and Urry's rule
18
+ - **Item Selection Strategies**: Maximum information criterion and Urry's rule
22
19
  - **Simulation Framework**: Comprehensive tools for CAT simulation and evaluation
23
20
  - **Real-world Application**: Direct transition from simulation to production testing
24
21
  - **Stopping Criteria**: Support for standard error and test length criteria
@@ -12,18 +12,21 @@ from .data.__pickle_context import PickleContext
12
12
  from .implementations.__default_implementation import DefaultImplementation
13
13
  from .implementations.__pre_test import PreTest
14
14
  from .implementations.__semi_implementation import SemiAdaptiveImplementation
15
- from .implementations.__test_assembler import TestAssembler
15
+ from .implementations.__test_assembler import TestAssembler, ContentBalancingArgs, ExposureControlArgs
16
16
 
17
17
  from .math.__gen_response_pattern import generate_response_pattern
18
18
  from .math.estimators.__ml_estimation import MLEstimator
19
19
  from .math.estimators.__bayes_modal_estimation import BayesModal
20
20
  from .math.estimators.__expect_a_posteriori import ExpectedAPosteriori
21
- from .math.estimators.__prior import Prior, NormalPrior, CustomPrior, CustomPriorException
21
+ from .math.estimators.__prior import Prior, NormalPrior, CustomPrior, CustomPriorException, SkewNormalPrior, EmpiricalPrior
22
22
  from .math.estimators.__functions.__estimators import probability_y0, probability_y1, maximize_likelihood_function, likelihood
23
23
  from .math.estimators.__functions.__bayes import maximize_posterior
24
24
  from .math.estimators.__test_information import test_information_function, item_information_function, prior_information_function
25
25
  from .math.item_selection.__maximum_information_criterion import maximum_information_criterion
26
26
  from .math.item_selection.__urrys_rule import urrys_rule
27
+ from .math.content_balancing.__weighted_penalty_model import WeightedPenaltyModel
28
+ from .math.content_balancing.__constraint import *
29
+ from .math.content_balancing.__functions import *
27
30
 
28
31
  from .models.__adaptive_test import AdaptiveTest
29
32
  from .models.__algorithm_exception import AlgorithmException
@@ -3,7 +3,8 @@ import csv
3
3
  import pathlib
4
4
  from ..models.__test_result import TestResult
5
5
  from ..services.__test_results_interface import ITestResults
6
- from dataclasses import fields
6
+ import json
7
+ import ast
7
8
 
8
9
 
9
10
  class CSVContext(ITestResults):
@@ -47,22 +48,34 @@ class CSVContext(ITestResults):
47
48
  file.close()
48
49
 
49
50
  def load(self) -> List[TestResult]:
50
- """Loads test results from a CSV file for a specific participant and simulation.
51
- Reads the CSV file located at `data/{simulation_id}/{participant_id}.csv`,
52
- parses each row into a `TestResult` object,
53
- and returns a list of these objects.
54
-
55
- Returns:
56
- List[TestResult]: A list of `TestResult` objects loaded from the CSV file.
51
+ """Loads results from the database.
52
+ The implementation of this method is required
53
+ by the interface. However, it does not have
54
+ any implemented functionality and will throw an error
55
+ if used.
56
+
57
+ Returns: List[TestResult]
57
58
  """
58
59
  foldername = f"data/{self.simulation_id}"
59
60
 
60
- fieldnames = list(fields(TestResult))
61
61
  test_results: list[TestResult] = []
62
62
  with open(f"{foldername}/{self.participant_id}.csv", "r", encoding="utf-8") as file:
63
- reader = csv.DictReader(file, fieldnames=fieldnames)
63
+ reader = csv.DictReader(file)
64
+ next(reader, None) # skip header row
64
65
  for row in reader:
65
66
  test_result = TestResult.from_dict(row)
67
+ # showed_item is read as a string representation of a Python
68
+ # literal (e.g. a dict). Use ast.literal_eval to safely parse
69
+ # it back into a dict. Fallback to a JSON-style conversion
70
+ # if literal_eval fails.
71
+ try:
72
+ test_result.showed_item = ast.literal_eval(str(test_result.showed_item))
73
+ except Exception:
74
+ json_string = str(test_result.showed_item).replace("'", '"').replace("None", "null")
75
+ try:
76
+ test_result.showed_item = json.loads(json_string)
77
+ except Exception:
78
+ test_result.showed_item = {}
66
79
  test_results.append(test_result)
67
80
  file.close()
68
81
  return test_results
@@ -0,0 +1,35 @@
1
+ from typing import Literal
2
+ from warnings import deprecated
3
+ from ..models.__test_item import TestItem
4
+ from ..models.__test_result import TestResult
5
+ from .__csv_context import CSVContext
6
+ from .__pickle_context import PickleContext
7
+
8
+
9
+ def read_prev_items(format: Literal["CSV", "PICKLE"],
10
+ test_id: str,
11
+ participant_ids: list[str]) -> list[TestItem]:
12
+ """Read previously administered items
13
+ to perform exposure control
14
+ """
15
+ all_shown_test_items = []
16
+
17
+ for part_id in participant_ids:
18
+ test_results = read_single_participant(test_id, part_id, format)
19
+ items = [TestItem.from_dict(res.showed_item) for res in test_results]
20
+
21
+ all_shown_test_items += items
22
+
23
+ return all_shown_test_items
24
+
25
+
26
+ @deprecated("This function should be replaced after merging"
27
+ " with revision branch")
28
+ def read_single_participant(test_id: str, participant_id: str, format: Literal["CSV", "PICKLE"]) -> list[TestResult]:
29
+ context: CSVContext | PickleContext
30
+ if format == "CSV":
31
+ context = CSVContext(test_id, participant_id)
32
+ return context.load()
33
+ if format == "PICKLE":
34
+ context = PickleContext(test_id, participant_id)
35
+ return context.load()
@@ -1,4 +1,4 @@
1
- from typing import List
1
+ from typing import List, cast
2
2
  import numpy as np
3
3
  from ..models.__test_item import TestItem
4
4
  import random
@@ -10,6 +10,7 @@ class PreTest:
10
10
  The pretest class can be used to draw items randomly from
11
11
  difficulty quantiles
12
12
  of the item pool.
13
+ This pretest can only be used with dichotomous IRT models and items.
13
14
 
14
15
  Args:
15
16
  items: Item pool
@@ -18,15 +19,21 @@ class PreTest:
18
19
  If not, the item selection will be drawn randomly, and you will not be able
19
20
  to reproduce the results.
20
21
 
22
+ Raises:
23
+ ValueError: Raised if the items are specified for polytomous IRT models.
24
+
21
25
  """
22
26
  self.items = items
23
27
  self.seed = seed
24
28
 
29
+ if any([isinstance(item.b, list) for item in items]):
30
+ raise ValueError("The pretest can only be used with dichotomous IRT models and items.")
31
+
25
32
  def calculate_quantiles(self) -> np.ndarray:
26
33
  """Calculates quantiles 0.25, 0.5, 0.75
27
34
  """
28
35
  # get difficulties
29
- difficulties: List[float] = [item.b for item in self.items]
36
+ difficulties: List[float] = [cast(float, item.b) for item in self.items]
30
37
 
31
38
  quantiles = np.array([])
32
39
  # calculate quantiles
@@ -50,7 +57,7 @@ class PreTest:
50
57
  """
51
58
  # select only items with difficulty in interval
52
59
  items_in_interval: List[TestItem] = [item for item in list(self.items)
53
- if lower < item.b <= upper]
60
+ if lower < cast(float, item.b) <= upper]
54
61
  # draw one item randomly
55
62
  if self.seed is not None:
56
63
  random.seed(self.seed)
@@ -0,0 +1,418 @@
1
+ from ..models.__item_selection_exception import ItemSelectionException
2
+ from ..math.content_balancing.__constraint import Constraint
3
+ from ..models.__adaptive_test import AdaptiveTest
4
+ from ..models.__test_item import TestItem
5
+ from ..services.__estimator_interface import IEstimator
6
+ from typing import Any, Type, TypedDict, Callable, Literal
7
+ from ..math.item_selection.__maximum_information_criterion import maximum_information_criterion
8
+ from ..models.__algorithm_exception import AlgorithmException
9
+ from ..implementations.__pre_test import PreTest
10
+ from ..models.__test_result import TestResult
11
+ from ..models.__misc import ResultOutputFormat
12
+ from ..services.__item_selection_protocol import ItemSelectionStrategy
13
+ from ..math.content_balancing.__content_balancing import CONTENT_BALANCING
14
+ from ..math.content_balancing.__weighted_penalty_model import WeightedPenaltyModel
15
+ from ..math.content_balancing.__maximum_priority_index import MaximumPriorityIndex
16
+ from ..math.estimators.__prior import Prior
17
+ from ..math.exposure_control.__exposure_control import EXPOSURE_CONTROL
18
+ from ..math.exposure_control.__randomesque import Randomesque
19
+ from ..math.exposure_control.__mpi_exposure_control import MaximumPriorityIndexExposureControl
20
+ import inspect
21
+
22
+
23
+ class EstimatorArgs(TypedDict):
24
+ prior: Prior | None
25
+ optimization_interval: tuple[float, float]
26
+ model: Literal["GRM", "GPCM"] | None
27
+
28
+
29
+ class ContentBalancingArgs(TypedDict):
30
+ constraints: list[Constraint] | None
31
+ """constraints that are applied to the item selection"""
32
+ constraint_weight: float | Callable[[AdaptiveTest], float] | None
33
+ """weight of the constraints
34
+ This can also be a function taking adaptive test as an input argument.
35
+ This allows the user to specify custom weight values depending
36
+ on the specific states and progress of the test."""
37
+ information_weight: float | Callable[[AdaptiveTest], float] | None
38
+ """weight of the item information
39
+ This can also be a function taking adaptive test as an input argument.
40
+ This allows the user to specify custom weight values depending
41
+ on the specific states and progress of the test."""
42
+
43
+
44
+ class ExposureControlArgs(TypedDict):
45
+ constraints: list[Constraint] | None
46
+ """Constraints applied for Maximum Priority Index exposure control."""
47
+ participant_ids: list[str] | None
48
+ """Participant ids to read the previously administered test in order to
49
+ perform exposure control (MPI only).
50
+ """
51
+ output_format: ResultOutputFormat | None
52
+ """Format in which the previous tests have been saved (MPI only)."""
53
+ n_items: int | None
54
+ """number of items to select for Randomesque items selection"""
55
+ seed: int | None
56
+ """random seed for the final item selection during Randomesque item selection"""
57
+
58
+
59
+ class TestAssembler(AdaptiveTest):
60
+ """
61
+ TestAssembler is a subclass of AdaptiveTest designed to assemble and administer adaptive tests,
62
+ optionally including a pretest phase. It supports customizable ability estimation and item selection strategies.
63
+
64
+ Methods:
65
+ estimate_ability_level():
66
+ Estimates the current ability level using the specified estimator and handles exceptions
67
+ for specific response patterns (all correct or all incorrect).
68
+
69
+ get_next_item() -> TestItem:
70
+ Selects the next item to administer using the specified item selection strategy.
71
+
72
+ run_test_once():
73
+ Runs a single iteration of the test, including an optional pretest phase. Handles item
74
+ administration, response collection, ability estimation, and result recording.
75
+
76
+ Attributes:
77
+ __ability_estimator: The estimator class for ability estimation.
78
+
79
+ __estimator_args: Arguments for the ability estimator.
80
+
81
+ __item_selector: The item selection strategy.
82
+
83
+ __item_selector_args: Arguments for the item selector.
84
+
85
+ __pretest: Whether to run a pretest phase.
86
+
87
+ __pretest_seed: Random seed for pretest item selection.
88
+ """
89
+
90
+ def __init__(self,
91
+ item_pool,
92
+ simulation_id,
93
+ participant_id,
94
+ ability_estimator: Type[IEstimator],
95
+ estimator_args: EstimatorArgs = {
96
+ "prior": None,
97
+ "optimization_interval": (-10, 10),
98
+ "model": None
99
+ },
100
+ item_selector: ItemSelectionStrategy = maximum_information_criterion, # type: ignore
101
+ item_selector_args: dict[str, Any] = {},
102
+ model_type: Literal["GRM", "GPCM"] | None = None,
103
+ content_balancing: None | CONTENT_BALANCING = None,
104
+ content_balancing_args: ContentBalancingArgs | None = None,
105
+ exposure_control: None | EXPOSURE_CONTROL = None,
106
+ exposure_control_args: None | ExposureControlArgs = None,
107
+ pretest: bool = False,
108
+ pretest_seed: int | None = None,
109
+ true_ability_level=None,
110
+ initial_ability_level=0,
111
+ simulation=True,
112
+ debug=False,
113
+ **kwargs):
114
+ """
115
+ Args:
116
+
117
+ item_pool: The pool of test items available for selection.
118
+
119
+ simulation_id: Identifier for the simulation run.
120
+
121
+ participant_id: Identifier for the participant.
122
+
123
+ ability_estimator (Type[IEstimator]): The estimator class used for ability estimation.
124
+
125
+ estimator_args (EstimatorArgs, optional):
126
+ Arguments for the ability estimator. Defaults to {"prior": None, "optimization_interval": (-10, 10)}.
127
+
128
+ item_selector (ItemSelectionStrategy, optional):
129
+ Function or strategy for selecting the next item. Defaults to maximum_information_criterion.
130
+
131
+ item_selector_args (dict[str, Any], optional): Arguments for the item selector. Defaults to {}.
132
+
133
+ content_balancing (CONTENT_BALANCING, optional): Selected content balancing strategy. Defaults to None.
134
+ If a content balancing strategy is specified, the item selection strategy will be ignored.
135
+
136
+ pretest (bool, optional): Whether to run a pretest phase before the main test. Defaults to False.
137
+
138
+ pretest_seed (int | None, optional): Random seed for pretest item selection. Defaults to None.
139
+
140
+ true_ability_level (optional): The true ability level of the participant (for simulation).
141
+
142
+ initial_ability_level (optional): The initial ability estimate. Defaults to 0.
143
+
144
+ simulation (bool, optional): Whether the test is run in simulation mode. Defaults to True.
145
+
146
+ debug (bool, optional): Whether to enable debug output. Defaults to False.
147
+
148
+ **kwargs: Additional keyword arguments passed to the AdaptiveTest superclass.
149
+ """
150
+ self.__ability_estimator = ability_estimator
151
+ self.__estimator_args = estimator_args
152
+ self.__item_selector = item_selector
153
+ self.__item_selector_args = item_selector_args
154
+ self.content_balancing = content_balancing
155
+ self.content_balancing_args = content_balancing_args
156
+ self.exposure_control = exposure_control
157
+ self.exposure_control_args = exposure_control_args
158
+ self.__pretest = pretest
159
+ self.__pretest_seed = pretest_seed
160
+ self.__estimator_args["model"] = model_type
161
+
162
+ super().__init__(item_pool,
163
+ simulation_id,
164
+ participant_id,
165
+ true_ability_level,
166
+ initial_ability_level,
167
+ simulation,
168
+ debug,
169
+ **kwargs)
170
+
171
+ def estimate_ability_level(self):
172
+ """
173
+ Estimates the ability level of a test-taker based on their response pattern and answered items.
174
+ This method uses the configured ability estimator to calculate the ability estimation and its standard error.
175
+ If an AlgorithmException occurs during estimation,
176
+ and all responses are identical (all correct or all incorrect),
177
+ it assigns a default estimation value (-10 for all incorrect, 10 for all correct)
178
+ and recalculates the standard error.
179
+ Otherwise, it raises an AlgorithmException with additional context.
180
+
181
+ Returns:
182
+ tuple[float, float]: A tuple containing the estimated ability level (float) and its standard error (float).
183
+
184
+ Raises:
185
+ AlgorithmException: If estimation fails for reasons other than all responses being identical.
186
+ """
187
+ # filter estimator args
188
+ sig = inspect.signature(self.__ability_estimator)
189
+ allowed = set(sig.parameters.keys())
190
+ filtered_estimator_args = {k: v for k, v in self.__estimator_args.items() if k in allowed}
191
+
192
+ # setup estimator
193
+ estimator = self.__ability_estimator(
194
+ self.response_pattern,
195
+ self.answered_items,
196
+ **filtered_estimator_args # type: ignore
197
+ )
198
+
199
+ try:
200
+ estimation = estimator.get_estimation()
201
+ standard_error = estimator.get_standard_error(estimation)
202
+ except AlgorithmException as exception:
203
+ # check if all responses are the same
204
+ if len(set(self.response_pattern)) == 1:
205
+ if self.response_pattern[0] == 0:
206
+ estimation = -10
207
+ elif self.response_pattern[0] == 1:
208
+ estimation = 10
209
+ standard_error = estimator.get_standard_error(estimation)
210
+
211
+ else:
212
+ raise AlgorithmException(f"""Something
213
+ when wrong when running {type(estimator)}""") from exception
214
+
215
+ return estimation, standard_error
216
+
217
+ def get_next_item(self) -> TestItem:
218
+ """
219
+ Selects and returns the next test item based on the current ability level and item selector strategy.
220
+ If a content balancing strategy is specified, the item selection strategy will be ignored.
221
+ Instead, the item selected by the content balancing strategy will be returned.
222
+ This also applies to exposure control.
223
+ However, content balancing and exposure control cannot be specified at the same time
224
+
225
+ Returns:
226
+ TestItem: The next item to be administered in the test, as determined by the item selector.
227
+
228
+ Raises:
229
+ Any exceptions raised by the item selector function.
230
+ """
231
+ item: TestItem | None
232
+ if self.content_balancing is None and self.exposure_control_args is None:
233
+ # content balancing an exposure control are not specified
234
+ # filter item selection args
235
+ sig = inspect.signature(self.__item_selector)
236
+ allowed = set(sig.parameters.keys())
237
+ filtered_item_selector_args = {k: v for k, v in self.__item_selector_args.items() if k in allowed}
238
+
239
+ item = self.__item_selector(
240
+ self.item_pool.test_items,
241
+ self.ability_level,
242
+ **filtered_item_selector_args
243
+ )
244
+ return item
245
+ elif self.content_balancing and self.exposure_control:
246
+ raise ValueError("Content balancing and exposure cannot be specified at the same time!")
247
+ # content balancing only
248
+ elif self.content_balancing and self.exposure_control is None:
249
+ if self.content_balancing_args is not None:
250
+ # which strategy has been selected
251
+ if self.content_balancing == "WeightedPenaltyModel":
252
+ # parse content balancing args
253
+
254
+ # setup wep
255
+ adaptive_test = self
256
+ wep = WeightedPenaltyModel(
257
+ adaptive_test,
258
+ constraints=self.check_args_are_not_none(
259
+ "constraints",
260
+ self.content_balancing_args["constraints"]),
261
+ constraint_weight=self.check_args_are_not_none(
262
+ "constraint_weights",
263
+ self.content_balancing_args["constraint_weight"]),
264
+ information_weight=self.check_args_are_not_none(
265
+ "information_weight",
266
+ self.content_balancing_args["information_weight"]))
267
+
268
+ item = wep.select_item()
269
+ if item is None:
270
+ raise ItemSelectionException(
271
+ f"""Something went wrong when selecting an item using {self.content_balancing}""")
272
+ else:
273
+ return item
274
+ elif self.content_balancing == "MaximumPriorityIndex":
275
+ adaptive_test = self
276
+ mpi = MaximumPriorityIndex(
277
+ adaptive_test,
278
+ constraints=self.check_args_are_not_none(
279
+ "constraints",
280
+ self.content_balancing_args["constraints"]))
281
+
282
+ item = mpi.select_item()
283
+
284
+ if item is None:
285
+ raise ItemSelectionException(
286
+ f"""Something went wrong when selecting an item using {self.content_balancing}""")
287
+ else:
288
+ return item
289
+ else:
290
+ raise ValueError("content_balancing_args cannot be None when using content balancing.")
291
+ # exposure control only
292
+ elif self.content_balancing is None and self.exposure_control:
293
+ if self.exposure_control_args is None:
294
+ raise ValueError("exposure_control_args cannot be None when using exposure control.")
295
+ # which strategy has been selected
296
+ if self.exposure_control == "Randomesque":
297
+ # Randomesque
298
+ adaptive_test = self
299
+ dict_keys = list(self.exposure_control_args.keys())
300
+ if "seed" not in dict_keys or "n_items" not in dict_keys:
301
+ raise ValueError("exposure_control_args are not correctly specified")
302
+
303
+ if self.exposure_control_args["n_items"] is not None:
304
+ randomesque = Randomesque(
305
+ adaptive_test=adaptive_test,
306
+ n_items=self.exposure_control_args["n_items"],
307
+ seed=self.exposure_control_args["seed"]
308
+ )
309
+ return randomesque.select_item()
310
+ else:
311
+ raise ValueError("n_items cannot be None.")
312
+
313
+ if self.exposure_control == "MaximumPriorityIndex":
314
+ if (self.exposure_control_args["participant_ids"]
315
+ is None or self.exposure_control_args["output_format"] is None):
316
+ raise ValueError("exposure_control_args are not correctly specified")
317
+ mpi = MaximumPriorityIndexExposureControl(
318
+ self,
319
+ constraints=self.exposure_control_args["constraints"],
320
+ participant_ids=self.exposure_control_args["participant_ids"],
321
+ format=self.exposure_control_args["output_format"]
322
+ )
323
+
324
+ selected_item = mpi.select_item()
325
+ if selected_item is None:
326
+ raise ItemSelectionException("Fatal! Not appropriated item was "
327
+ "selected using MPI for exposure control")
328
+ else:
329
+ return selected_item
330
+ raise ValueError(f"Something went wrong when selecting an item using {self.content_balancing}.")
331
+
332
+ def run_test_once(self):
333
+ """
334
+ Executes a single run of the test, including optional pretest logic.
335
+ If pretesting is enabled, this method:
336
+ - Selects a random quantile of items from the item pool using a PreTest instance.
337
+ - For each selected item:
338
+ - Obtains a response (either simulated or real).
339
+ - Appends the response and item to the respective lists.
340
+ - Removes the item from the item pool.
341
+ - Estimates the ability level and standard error after pretest responses.
342
+ - Records test results for each pretest item, with the final item including the first ability estimation.
343
+
344
+ Returns:
345
+ The result of the superclass's run_test_once() method.
346
+ """
347
+ # check if to run pretest
348
+ if self.__pretest is True:
349
+ pretest = PreTest(
350
+ self.item_pool.test_items,
351
+ self.__pretest_seed
352
+ )
353
+ # get selected items
354
+ random_items = pretest.select_random_item_quantile()
355
+ for item in random_items:
356
+ if self.simulation is True:
357
+ response = self.item_pool.get_item_response(item)
358
+ else:
359
+ # not simulation
360
+ response = self.get_response(item)
361
+
362
+ if self.DEBUG:
363
+ print(f"Response: {response}")
364
+ # add response to response pattern
365
+ self.response_pattern.append(response)
366
+ # add item to answered items list
367
+ self.answered_items.append(item)
368
+
369
+ # remove items
370
+ self.item_pool.delete_item(item)
371
+ # estimate ability level
372
+ estimation, sd_error = self.estimate_ability_level()
373
+ self.ability_level = estimation
374
+ self.standard_error = sd_error
375
+ # create test results for all n-1 random items
376
+ for i in range(0, len(random_items) - 1):
377
+ result = TestResult(
378
+ ability_estimation=float("nan"),
379
+ standard_error=float("nan"),
380
+ showed_item=random_items[i].as_dict(),
381
+ response=self.response_pattern[i],
382
+ test_id=self.simulation_id,
383
+ true_ability_level=self.true_ability_level if self.true_ability_level is not None else float("NaN"),
384
+ )
385
+ # append to memory
386
+ self.test_results.append(result)
387
+
388
+ # create test result for first ability estimation
389
+ intermediate_result = TestResult(
390
+ ability_estimation=self.ability_level,
391
+ standard_error=self.standard_error,
392
+ showed_item=random_items[-1].as_dict(),
393
+ response=self.response_pattern[-1],
394
+ test_id=self.simulation_id,
395
+ true_ability_level=self.true_ability_level if self.true_ability_level is not None else float("NaN"),
396
+ )
397
+ self.test_results.append(intermediate_result)
398
+
399
+ return super().run_test_once()
400
+
401
+ def check_args_are_not_none(self, key: str, x: Any | None) -> Any:
402
+ """This functions checks if an object is none.
403
+ If not a ValueError is raised.
404
+
405
+ Args:
406
+ key (str): parameter name where the object is typically assigned
407
+ x (Any | None): object
408
+
409
+ Raises:
410
+ ValueError: raised if the object is None.
411
+
412
+ Returns:
413
+ Any: object
414
+ """
415
+ if x is not None:
416
+ return x
417
+ else:
418
+ raise ValueError(f"{key} cannot be None.")