adaptivetesting 1.0.0__py3-none-any.whl
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.
- adaptivetesting/__init__.py +7 -0
- adaptivetesting/data/__init__.py +2 -0
- adaptivetesting/data/__pickle_context.py +51 -0
- adaptivetesting/data/__sqlite_context.py +76 -0
- adaptivetesting/implementations/__default_implementation.py +72 -0
- adaptivetesting/implementations/__init__.py +3 -0
- adaptivetesting/implementations/__pre_test.py +83 -0
- adaptivetesting/implementations/__semi_implementation.py +134 -0
- adaptivetesting/math/__init__.py +2 -0
- adaptivetesting/math/__standard_error.py +59 -0
- adaptivetesting/math/__test_information.py +43 -0
- adaptivetesting/models/__adaptive_test.py +183 -0
- adaptivetesting/models/__algorithm_exception.py +3 -0
- adaptivetesting/models/__init__.py +7 -0
- adaptivetesting/models/__item_pool.py +246 -0
- adaptivetesting/models/__item_selection_exception.py +3 -0
- adaptivetesting/models/__test_item.py +29 -0
- adaptivetesting/models/__test_result.py +23 -0
- adaptivetesting/services/__estimator_interface.py +40 -0
- adaptivetesting/services/__init__.py +2 -0
- adaptivetesting/services/__test_results_interface.py +27 -0
- adaptivetesting/simulation/__init__.py +1 -0
- adaptivetesting/simulation/__simulation.py +75 -0
- adaptivetesting/tests/__init__.py +0 -0
- adaptivetesting/tests/test_adaptive_test.py +67 -0
- adaptivetesting/tests/test_bayes_modal.py +31 -0
- adaptivetesting/tests/test_code_examples.py +22 -0
- adaptivetesting/tests/test_information_test.py +38 -0
- adaptivetesting/tests/test_load_test_items.py +112 -0
- adaptivetesting/tests/test_mle.py +73 -0
- adaptivetesting/tests/test_pretest.py +39 -0
- adaptivetesting/tests/test_standard_error.py +57 -0
- adaptivetesting/tests/test_urrys_rule.py +28 -0
- adaptivetesting-1.0.0.dist-info/METADATA +108 -0
- adaptivetesting-1.0.0.dist-info/RECORD +38 -0
- adaptivetesting-1.0.0.dist-info/WHEEL +5 -0
- adaptivetesting-1.0.0.dist-info/licenses/LICENSE +372 -0
- adaptivetesting-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from .__test_item import TestItem
|
|
3
|
+
from ..math.item_selection.__urrys_rule import urrys_rule
|
|
4
|
+
from ..math.__standard_error import standard_error
|
|
5
|
+
import abc
|
|
6
|
+
from .__test_result import TestResult
|
|
7
|
+
from .__item_pool import ItemPool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AdaptiveTest(abc.ABC):
|
|
11
|
+
def __init__(self, item_pool: ItemPool,
|
|
12
|
+
simulation_id: str,
|
|
13
|
+
participant_id: int,
|
|
14
|
+
true_ability_level: float,
|
|
15
|
+
initial_ability_level: float = 0,
|
|
16
|
+
simulation: bool = True,
|
|
17
|
+
DEBUG=False):
|
|
18
|
+
"""Abstract implementation of an adaptive test.
|
|
19
|
+
All abstract methods have to be overridden
|
|
20
|
+
to create an instance of this class.
|
|
21
|
+
|
|
22
|
+
Abstract methods:
|
|
23
|
+
- estimate_ability_level
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
item_pool (ItemPool): item pool used for the test
|
|
27
|
+
|
|
28
|
+
simulation_id (str): simulation id
|
|
29
|
+
|
|
30
|
+
participant_id (int): participant id
|
|
31
|
+
|
|
32
|
+
true_ability_level (float): true ability level (must always be set)
|
|
33
|
+
|
|
34
|
+
initial_ability_level (float): initially assumed ability level
|
|
35
|
+
|
|
36
|
+
simulation (bool): will the test be simulated
|
|
37
|
+
|
|
38
|
+
DEBUG (bool): enables debug mode
|
|
39
|
+
"""
|
|
40
|
+
self.true_ability_level: float = true_ability_level
|
|
41
|
+
self.simulation_id = simulation_id
|
|
42
|
+
self.participant_id: int = participant_id
|
|
43
|
+
# set start values
|
|
44
|
+
self.ability_level = initial_ability_level
|
|
45
|
+
self.answered_items: List[TestItem] = []
|
|
46
|
+
self.response_pattern: List[int] = []
|
|
47
|
+
self.test_results: List[TestResult] = []
|
|
48
|
+
# load items
|
|
49
|
+
self.item_pool = item_pool
|
|
50
|
+
|
|
51
|
+
# debug
|
|
52
|
+
self.DEBUG = DEBUG
|
|
53
|
+
self.simulation = simulation
|
|
54
|
+
|
|
55
|
+
def get_item_difficulties(self) -> List[float]:
|
|
56
|
+
"""
|
|
57
|
+
Returns:
|
|
58
|
+
List[float]: difficulties of items in the item pool
|
|
59
|
+
"""
|
|
60
|
+
return [item.b for item in self.item_pool.test_items]
|
|
61
|
+
|
|
62
|
+
def get_answered_items_difficulties(self) -> List[float]:
|
|
63
|
+
"""
|
|
64
|
+
Returns:
|
|
65
|
+
List[float]: difficulties of answered items
|
|
66
|
+
"""
|
|
67
|
+
return [item.b for item in self.answered_items]
|
|
68
|
+
|
|
69
|
+
def get_answered_items(self) -> List[TestItem]:
|
|
70
|
+
"""
|
|
71
|
+
Returns:
|
|
72
|
+
List[TestItem]: answered items
|
|
73
|
+
"""
|
|
74
|
+
return self.answered_items
|
|
75
|
+
|
|
76
|
+
def get_ability_se(self) -> float:
|
|
77
|
+
"""
|
|
78
|
+
Calculate the current standard error
|
|
79
|
+
of the ability estimation.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
float: standard error of the ability estimation
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
answered_items= self.get_answered_items()
|
|
86
|
+
return standard_error(answered_items, self.ability_level)
|
|
87
|
+
|
|
88
|
+
def get_next_item(self) -> TestItem:
|
|
89
|
+
"""Select next item using Urry's rule.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
TestItem: selected item
|
|
93
|
+
"""
|
|
94
|
+
item = urrys_rule(self.item_pool.test_items, self.ability_level)
|
|
95
|
+
return item
|
|
96
|
+
|
|
97
|
+
@abc.abstractmethod
|
|
98
|
+
def estimate_ability_level(self) -> float:
|
|
99
|
+
"""
|
|
100
|
+
Estimates ability level.
|
|
101
|
+
The method has to be implemented by subclasses.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
float: estimated ability level
|
|
105
|
+
"""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def get_response(self, item: TestItem) -> int:
|
|
109
|
+
"""If the adaptive test is not used for simulation.
|
|
110
|
+
This method is used to get user feedback.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
item (TestItem): test item shown to the participant
|
|
114
|
+
|
|
115
|
+
Return:
|
|
116
|
+
int: participant's response
|
|
117
|
+
"""
|
|
118
|
+
raise NotImplementedError("This functionality is not implemented by default.")
|
|
119
|
+
|
|
120
|
+
def run_test_once(self):
|
|
121
|
+
"""
|
|
122
|
+
Runs the test procedure once.
|
|
123
|
+
Saves the result to test_results of
|
|
124
|
+
the current instance.
|
|
125
|
+
"""
|
|
126
|
+
# get item
|
|
127
|
+
item = self.get_next_item()
|
|
128
|
+
if item is not None:
|
|
129
|
+
if self.DEBUG:
|
|
130
|
+
print(f"Selected {item.b} for an ability level of {self.ability_level}.")
|
|
131
|
+
|
|
132
|
+
# check if simulation is running
|
|
133
|
+
response = None
|
|
134
|
+
if self.simulation is True:
|
|
135
|
+
response = self.item_pool.get_item_response(item)
|
|
136
|
+
else:
|
|
137
|
+
# not simulation
|
|
138
|
+
response = self.get_response(item)
|
|
139
|
+
|
|
140
|
+
if self.DEBUG:
|
|
141
|
+
print(f"Response: {response}")
|
|
142
|
+
|
|
143
|
+
# add response to response pattern
|
|
144
|
+
self.response_pattern.append(response)
|
|
145
|
+
# add item to answered items list
|
|
146
|
+
self.answered_items.append(item)
|
|
147
|
+
|
|
148
|
+
# estimate ability level
|
|
149
|
+
estimation = self.estimate_ability_level()
|
|
150
|
+
|
|
151
|
+
# update estimated ability level
|
|
152
|
+
self.ability_level = estimation
|
|
153
|
+
if self.DEBUG:
|
|
154
|
+
print(f"New estimation is {self.ability_level}")
|
|
155
|
+
# remove item from item pool
|
|
156
|
+
self.item_pool.delete_item(item)
|
|
157
|
+
if self.DEBUG:
|
|
158
|
+
print(f"Now, there are only {len(self.item_pool.test_items)} left in the item pool.")
|
|
159
|
+
# create result
|
|
160
|
+
result: TestResult = TestResult(
|
|
161
|
+
ability_estimation=estimation,
|
|
162
|
+
standard_error=self.get_ability_se(),
|
|
163
|
+
showed_item=item.b,
|
|
164
|
+
response=response,
|
|
165
|
+
test_id=self.simulation_id,
|
|
166
|
+
true_ability_level=self.true_ability_level
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# add result to memory
|
|
170
|
+
self.test_results.append(result)
|
|
171
|
+
|
|
172
|
+
def check_se_criterion(self, value: float) -> bool:
|
|
173
|
+
if self.get_ability_se() <= value:
|
|
174
|
+
return True
|
|
175
|
+
else:
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
def check_length_criterion(self, value: float) -> bool:
|
|
179
|
+
if len(self.answered_items) >= value:
|
|
180
|
+
return True
|
|
181
|
+
else:
|
|
182
|
+
return False
|
|
183
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from .__adaptive_test import AdaptiveTest
|
|
2
|
+
from .__algorithm_exception import AlgorithmException
|
|
3
|
+
from .__item_pool import ItemPool
|
|
4
|
+
from .__item_selection_exception import ItemSelectionException
|
|
5
|
+
from .__test_item import TestItem
|
|
6
|
+
from .__test_result import TestResult
|
|
7
|
+
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
from .__test_item import TestItem
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
from pandas import DataFrame
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ItemPool:
|
|
7
|
+
def __init__(self,
|
|
8
|
+
test_items: List[TestItem],
|
|
9
|
+
simulated_responses: List[int] | None = None):
|
|
10
|
+
"""An item pool has to be created for an adaptive test.
|
|
11
|
+
For that, a list of test items has to be provided. If the package is used
|
|
12
|
+
to simulate adaptive tests, simulated responses have to be supplied as well.
|
|
13
|
+
The responses are matched to the items internally.
|
|
14
|
+
Therefore, both have to be in the same order.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
test_items (List[TestItem]): A list of test items. Necessary for any adaptive test.
|
|
18
|
+
|
|
19
|
+
simulated_responses (List[int]): A list of simulated responses. Required for CAT simulations.
|
|
20
|
+
"""
|
|
21
|
+
self.test_items: List[TestItem] = test_items
|
|
22
|
+
self.simulated_responses: List[int] | None = simulated_responses
|
|
23
|
+
|
|
24
|
+
def get_item_by_index(self, index: int) -> Tuple[TestItem, int] | TestItem:
|
|
25
|
+
"""Returns item and if defined the simulated response.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
index (int): Index of the test item in the item pool to return.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
TestItem or (TestItem, Simulated Response)
|
|
32
|
+
"""
|
|
33
|
+
selected_item = self.test_items[index]
|
|
34
|
+
if self.simulated_responses is not None:
|
|
35
|
+
simulated_response = self.simulated_responses[index]
|
|
36
|
+
return selected_item, simulated_response
|
|
37
|
+
else:
|
|
38
|
+
return selected_item
|
|
39
|
+
|
|
40
|
+
def get_item_by_item(self, item: TestItem) -> Tuple[TestItem, int] | TestItem:
|
|
41
|
+
"""Returns item and if defined the simulated response.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
item (TestItem): item to return.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
TestItem or (TestItem, Simulated Response)
|
|
48
|
+
"""
|
|
49
|
+
index = self.test_items.index(item)
|
|
50
|
+
selected_item = self.test_items[index]
|
|
51
|
+
if self.simulated_responses is not None:
|
|
52
|
+
simulated_response = self.simulated_responses[index]
|
|
53
|
+
return selected_item, simulated_response
|
|
54
|
+
else:
|
|
55
|
+
return selected_item
|
|
56
|
+
|
|
57
|
+
def get_item_response(self, item: TestItem) -> int:
|
|
58
|
+
"""
|
|
59
|
+
Gets the simulated response to an item if available.
|
|
60
|
+
A `ValueError` will be raised if a simulated response is not available.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
item (TestItem): item to get the corresponding response
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
(int): response (either `0` or `1`)
|
|
67
|
+
"""
|
|
68
|
+
if self.simulated_responses is None:
|
|
69
|
+
raise ValueError("Simulated responses not provided")
|
|
70
|
+
else:
|
|
71
|
+
i, res = self.get_item_by_item(item) # type: ignore
|
|
72
|
+
return res
|
|
73
|
+
|
|
74
|
+
def delete_item(self, item: TestItem) -> None:
|
|
75
|
+
"""Deletes item from item pool.
|
|
76
|
+
If simulated responses are defined, they will be deleted as well.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
item (TestItem): The test item to delete.
|
|
80
|
+
"""
|
|
81
|
+
# get index
|
|
82
|
+
index = self.test_items.index(item)
|
|
83
|
+
# remove item at index
|
|
84
|
+
self.test_items.pop(index)
|
|
85
|
+
# remove response at index
|
|
86
|
+
if self.simulated_responses is not None:
|
|
87
|
+
self.simulated_responses.pop(index)
|
|
88
|
+
|
|
89
|
+
# STATIC LOAD METHODS
|
|
90
|
+
@staticmethod
|
|
91
|
+
def load_from_list(
|
|
92
|
+
b: List[float],
|
|
93
|
+
a: List[float] | None = None,
|
|
94
|
+
c: List[float] | None = None,
|
|
95
|
+
d: List[float] | None = None,
|
|
96
|
+
simulated_responses: List[int] | None = None
|
|
97
|
+
) -> "ItemPool":
|
|
98
|
+
"""
|
|
99
|
+
Creates test items from a list of floats.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
a (List[float]): discrimination parameter
|
|
103
|
+
|
|
104
|
+
b (List[float]): difficulty parameter
|
|
105
|
+
|
|
106
|
+
c (List[float]): guessing parameter
|
|
107
|
+
|
|
108
|
+
d (List[float]): slipping parameter
|
|
109
|
+
|
|
110
|
+
simulated_responses (List[int]): simulated responses
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List[TestItem]: item pool
|
|
114
|
+
|
|
115
|
+
"""
|
|
116
|
+
items: List[TestItem] = []
|
|
117
|
+
|
|
118
|
+
for difficulty in b:
|
|
119
|
+
item = TestItem()
|
|
120
|
+
item.b = difficulty
|
|
121
|
+
items.append(item)
|
|
122
|
+
|
|
123
|
+
# check if a, b, c, d are the same length
|
|
124
|
+
if a is not None:
|
|
125
|
+
if len(a) != len(b):
|
|
126
|
+
raise ValueError("Length of a and b has to be the same.")
|
|
127
|
+
for i, discrimination in enumerate(a):
|
|
128
|
+
items[i].a = discrimination
|
|
129
|
+
|
|
130
|
+
if c is not None:
|
|
131
|
+
if len(c) != len(b):
|
|
132
|
+
raise ValueError("Length of c and b has to be the same.")
|
|
133
|
+
for i, guessing in enumerate(c):
|
|
134
|
+
items[i].c = guessing
|
|
135
|
+
|
|
136
|
+
if d is not None:
|
|
137
|
+
if len(d) != len(b):
|
|
138
|
+
raise ValueError("Length of d and b has to be the same.")
|
|
139
|
+
for i, slipping in enumerate(d):
|
|
140
|
+
items[i].d = slipping
|
|
141
|
+
|
|
142
|
+
item_pool = ItemPool(items)
|
|
143
|
+
item_pool.simulated_responses = simulated_responses
|
|
144
|
+
|
|
145
|
+
return item_pool
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def load_from_dict(source: dict[str, List[float]],
|
|
149
|
+
simulated_responses: List[int] | None = None) -> "ItemPool":
|
|
150
|
+
"""Creates test items from a dictionary.
|
|
151
|
+
The dictionary has to have the following keys:
|
|
152
|
+
|
|
153
|
+
- a
|
|
154
|
+
- b
|
|
155
|
+
- c
|
|
156
|
+
- d
|
|
157
|
+
each containing a list of float.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
source (dict[str, List[float]]): item pool dictionary
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List[TestItem]: item pool
|
|
164
|
+
"""
|
|
165
|
+
a = source.get("a")
|
|
166
|
+
b = source.get("b")
|
|
167
|
+
c = source.get("c")
|
|
168
|
+
d = source.get("d")
|
|
169
|
+
|
|
170
|
+
# check none
|
|
171
|
+
if a is None:
|
|
172
|
+
raise ValueError("a cannot be None")
|
|
173
|
+
|
|
174
|
+
if b is None:
|
|
175
|
+
raise ValueError("b cannot be None")
|
|
176
|
+
|
|
177
|
+
if c is None:
|
|
178
|
+
raise ValueError("c cannot be None")
|
|
179
|
+
|
|
180
|
+
if d is None:
|
|
181
|
+
raise ValueError("d cannot be None")
|
|
182
|
+
|
|
183
|
+
# check if a, b, c, and d have the same length
|
|
184
|
+
if not (len(a) == len(b) == len(c) == len(d)):
|
|
185
|
+
raise ValueError("All lists in the source dictionary must have the same length")
|
|
186
|
+
|
|
187
|
+
n_items = len(b)
|
|
188
|
+
items: List[TestItem] = []
|
|
189
|
+
for i in range(n_items):
|
|
190
|
+
item = TestItem()
|
|
191
|
+
item.a = a[i]
|
|
192
|
+
item.b = b[i]
|
|
193
|
+
item.c = c[i]
|
|
194
|
+
item.d = d[i]
|
|
195
|
+
|
|
196
|
+
items.append(item)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
item_pool = ItemPool(items, simulated_responses)
|
|
200
|
+
|
|
201
|
+
return item_pool
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def load_from_dataframe(source: DataFrame) -> "ItemPool":
|
|
205
|
+
"""Creates item pool from a pandas DataFrame.
|
|
206
|
+
Required columns are: `a`, `b`, `c`, `d`.
|
|
207
|
+
Each column has to contain float values.
|
|
208
|
+
A `simulated_responses` (int values) column can be added to
|
|
209
|
+
the DataFrame to provide simulated responses.
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
source (DataFrame): _description_
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
ItemPool: _description_
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
# check if columns are present
|
|
220
|
+
if "a" not in source.columns:
|
|
221
|
+
raise ValueError("Column 'a' not found.")
|
|
222
|
+
|
|
223
|
+
if "b" not in source.columns:
|
|
224
|
+
raise ValueError("Column 'b' not found.")
|
|
225
|
+
|
|
226
|
+
if "c" not in source.columns:
|
|
227
|
+
raise ValueError("Column 'c' not found.")
|
|
228
|
+
|
|
229
|
+
if "d" not in source.columns:
|
|
230
|
+
raise ValueError("Column 'd' not found.")
|
|
231
|
+
|
|
232
|
+
# get values
|
|
233
|
+
a: List[float] = source["a"].values.tolist() # type: ignore
|
|
234
|
+
b: List[float] = source["b"].values.tolist() # type: ignore
|
|
235
|
+
c: List[float] = source["c"].values.tolist() # type: ignore
|
|
236
|
+
d: List[float] = source["d"].values.tolist() # type: ignore
|
|
237
|
+
|
|
238
|
+
# create item pool
|
|
239
|
+
item_pool = ItemPool.load_from_list(a=a, b=b, c=c, d=d)
|
|
240
|
+
|
|
241
|
+
# check if simulated responses are present
|
|
242
|
+
if "simulated_responses" in source.columns:
|
|
243
|
+
simulated_responses: List[int] = source["simulated_responses"].values.tolist() # type: ignore
|
|
244
|
+
item_pool.simulated_responses = simulated_responses
|
|
245
|
+
|
|
246
|
+
return item_pool
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestItem:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
"""Representation of a test item in the item pool.
|
|
8
|
+
The format is equal to the implementation in catR.
|
|
9
|
+
|
|
10
|
+
Properties:
|
|
11
|
+
- a (float):
|
|
12
|
+
- b (float): difficulty
|
|
13
|
+
- c (float):
|
|
14
|
+
- d (float):
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
self.id: int | None = None
|
|
18
|
+
self.a: float = 1
|
|
19
|
+
self.b: float = float("nan")
|
|
20
|
+
self.c: float = 0
|
|
21
|
+
self.d: float = 1
|
|
22
|
+
|
|
23
|
+
def as_dict(self) -> dict[str, float]:
|
|
24
|
+
return {
|
|
25
|
+
"a": self.a,
|
|
26
|
+
"b": self.b,
|
|
27
|
+
"c": self.c,
|
|
28
|
+
"d": self.d
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Dict
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class TestResult:
|
|
7
|
+
"""Representation of simulation test results"""
|
|
8
|
+
test_id: str
|
|
9
|
+
ability_estimation: float
|
|
10
|
+
standard_error: float
|
|
11
|
+
showed_item: float
|
|
12
|
+
response: int
|
|
13
|
+
true_ability_level: float
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def from_dict(dictionary: Dict) -> 'TestResult':
|
|
17
|
+
"""Create a TestResult from a dictionary
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
dictionary: with the fields `test_id`, `ability_estimation`, `standard_error`, `showed_item`, `response`,
|
|
21
|
+
`true_ability_level`
|
|
22
|
+
"""
|
|
23
|
+
return TestResult(**dictionary)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
import jax.numpy as np
|
|
4
|
+
from ..models.__test_item import TestItem
|
|
5
|
+
|
|
6
|
+
class IEstimator(ABC):
|
|
7
|
+
def __init__(self,
|
|
8
|
+
response_pattern: List[int] | np.ndarray,
|
|
9
|
+
items: List[TestItem],
|
|
10
|
+
optimization_interval: Tuple[float, float] = (-10, 10)):
|
|
11
|
+
"""This is the interface required for every possible
|
|
12
|
+
estimator.
|
|
13
|
+
Any estimator inherits from this class and implements
|
|
14
|
+
the `get_estimation` method.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
response_pattern (List[int]): list of responses (0: wrong, 1:right)
|
|
18
|
+
|
|
19
|
+
items (List[TestItem]): list of answered items
|
|
20
|
+
"""
|
|
21
|
+
if type(response_pattern) is not np.ndarray:
|
|
22
|
+
self.response_pattern = np.array(response_pattern)
|
|
23
|
+
else:
|
|
24
|
+
self.response_pattern = response_pattern
|
|
25
|
+
self.optimization_interval = optimization_interval
|
|
26
|
+
|
|
27
|
+
# convert items to parameter arrays
|
|
28
|
+
self.a = np.array([i.a for i in items])
|
|
29
|
+
self.b = np.array([i.b for i in items])
|
|
30
|
+
self.c = np.array([i.c for i in items])
|
|
31
|
+
self.d = np.array([i.d for i in items])
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
def get_estimation(self) -> float:
|
|
35
|
+
"""Get the currently estimated ability.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
float: ability
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from ..models.__test_result import TestResult
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ITestResults(ABC):
|
|
7
|
+
|
|
8
|
+
def __init__(self, simulation_id: str, participant_id: int):
|
|
9
|
+
"""Interface for saving and reading test results.
|
|
10
|
+
This interface may mainly be used for saving simulation results.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
simulation_id (str): The simulation ID. Name of the results file.
|
|
14
|
+
|
|
15
|
+
participant_id (int): The participant ID.
|
|
16
|
+
"""
|
|
17
|
+
self.simulation_id = simulation_id
|
|
18
|
+
self.filename = f"data/{self.simulation_id}.db"
|
|
19
|
+
self.participant_id: int = participant_id
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def save(self, test_results: List[TestResult]) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def load(self) -> List[TestResult]:
|
|
27
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .__simulation import *
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from ..models.__adaptive_test import AdaptiveTest
|
|
2
|
+
from ..data.__sqlite_context import SQLiteContext
|
|
3
|
+
from ..data.__pickle_context import PickleContext
|
|
4
|
+
from ..services.__test_results_interface import ITestResults
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ResultOutputFormat(Enum):
|
|
9
|
+
"""
|
|
10
|
+
Enum for selecting the output format for
|
|
11
|
+
the test results
|
|
12
|
+
"""
|
|
13
|
+
SQLITE = 1
|
|
14
|
+
PICKLE = 2
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class StoppingCriterion(Enum):
|
|
18
|
+
"""Enum for selecting the stopping criterion
|
|
19
|
+
for the adaptive test"""
|
|
20
|
+
SE = 1
|
|
21
|
+
LENGTH = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Simulation:
|
|
25
|
+
def __init__(self,
|
|
26
|
+
test: AdaptiveTest,
|
|
27
|
+
test_result_output: ResultOutputFormat):
|
|
28
|
+
"""
|
|
29
|
+
This class can be used for simulating CAT.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
test (AdaptiveTest): instance of an adaptive test implementation (see implementations module)
|
|
33
|
+
|
|
34
|
+
test_result_output (ResultOutputFormat): test results output format
|
|
35
|
+
"""
|
|
36
|
+
self.test = test
|
|
37
|
+
self.test_result_output = test_result_output
|
|
38
|
+
|
|
39
|
+
def simulate(self,
|
|
40
|
+
criterion: StoppingCriterion = StoppingCriterion.SE,
|
|
41
|
+
value: float = 0.4):
|
|
42
|
+
"""Runs test until the stopping criterion is met.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
criterion (StoppingCriterion): selected stopping criterion
|
|
46
|
+
|
|
47
|
+
value (float): either standard error value or test length value that has to be met by the test
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
stop_test = False
|
|
51
|
+
while stop_test is False:
|
|
52
|
+
# run test
|
|
53
|
+
self.test.run_test_once()
|
|
54
|
+
# check available items
|
|
55
|
+
if len(self.test.item_pool.test_items) == 0:
|
|
56
|
+
stop_test = True
|
|
57
|
+
else:
|
|
58
|
+
if criterion == StoppingCriterion.SE:
|
|
59
|
+
stop_test = self.test.check_se_criterion(value)
|
|
60
|
+
elif criterion == StoppingCriterion.LENGTH:
|
|
61
|
+
stop_test = self.test.check_length_criterion(value)
|
|
62
|
+
|
|
63
|
+
def save_test_results(self):
|
|
64
|
+
"""Saves the test results to the specified output format."""
|
|
65
|
+
data_context: ITestResults
|
|
66
|
+
if self.test_result_output == ResultOutputFormat.PICKLE:
|
|
67
|
+
data_context = PickleContext(simulation_id=self.test.simulation_id,
|
|
68
|
+
participant_id=self.test.participant_id)
|
|
69
|
+
else:
|
|
70
|
+
data_context = SQLiteContext(
|
|
71
|
+
simulation_id=self.test.simulation_id,
|
|
72
|
+
participant_id=self.test.participant_id
|
|
73
|
+
)
|
|
74
|
+
# save results
|
|
75
|
+
data_context.save(self.test.test_results)
|
|
File without changes
|