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,51 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import pickle
|
|
3
|
+
import pathlib
|
|
4
|
+
from ..models.__test_result import TestResult
|
|
5
|
+
from ..services.__test_results_interface import ITestResults
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PickleContext(ITestResults):
|
|
9
|
+
def __init__(self,
|
|
10
|
+
simulation_id: str,
|
|
11
|
+
participant_id: int):
|
|
12
|
+
"""Implementation of the ITestResults interface for
|
|
13
|
+
saving test results to the pickle format.
|
|
14
|
+
The resulting pickle file <simulation_id>.pickle
|
|
15
|
+
will be of the standard pickle format which depends
|
|
16
|
+
on the used python version.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
simulation_id (str): Not used but required by the interface
|
|
20
|
+
|
|
21
|
+
participant_id (int): participant id and table name
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(simulation_id, participant_id)
|
|
24
|
+
|
|
25
|
+
def save(self, test_results: List[TestResult]) -> None:
|
|
26
|
+
"""Saves a list of test results to a pickle binary file
|
|
27
|
+
(<participant_id>.pickle).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
test_results (List[TestResult]): list of test results
|
|
31
|
+
"""
|
|
32
|
+
dir_name = self.simulation_id
|
|
33
|
+
|
|
34
|
+
# create directory if it does not already exist
|
|
35
|
+
path = pathlib.Path(f"data/{dir_name}")
|
|
36
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
# write results in file
|
|
38
|
+
with open(f"data/{dir_name}/{str(self.participant_id)}.pickle", "wb") as file:
|
|
39
|
+
pickle.dump(test_results, file)
|
|
40
|
+
file.close()
|
|
41
|
+
|
|
42
|
+
def load(self) -> List[TestResult]:
|
|
43
|
+
"""Loads results from the database.
|
|
44
|
+
The implementation of this method is required
|
|
45
|
+
by the interface. However, is does not have
|
|
46
|
+
any implemented functionality and will throw an error
|
|
47
|
+
if used.
|
|
48
|
+
|
|
49
|
+
Returns: List[TestResult]
|
|
50
|
+
"""
|
|
51
|
+
raise NotImplementedError("This function is not implemented.")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from ..models.__adaptive_test import AdaptiveTest
|
|
3
|
+
from ..models.__test_result import TestResult
|
|
4
|
+
from ..services.__test_results_interface import ITestResults
|
|
5
|
+
import sqlite3
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SQLiteContext(ITestResults):
|
|
9
|
+
def __init__(self, simulation_id: str, participant_id: int):
|
|
10
|
+
"""Implementation of the ITestResults interface for
|
|
11
|
+
saving test results to a SQLITE database.
|
|
12
|
+
The resulting sqlite file <simulation_id>.db
|
|
13
|
+
will be of the SQLITE3 format.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
simulation_id (str): db filename
|
|
17
|
+
|
|
18
|
+
participant_id (int): participant id and table name
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(simulation_id, participant_id)
|
|
21
|
+
|
|
22
|
+
def save(self, test_results: List[TestResult]) -> None:
|
|
23
|
+
"""Saves a list of test results to the database
|
|
24
|
+
in the table <participant_id>.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
test_results (List[TestResult]): list of test results
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
con = sqlite3.connect(self.filename)
|
|
33
|
+
except sqlite3.OperationalError:
|
|
34
|
+
con = sqlite3.connect(f"../{self.filename}")
|
|
35
|
+
|
|
36
|
+
cur: sqlite3.Cursor = con.cursor()
|
|
37
|
+
# create table
|
|
38
|
+
self._create_table(cur)
|
|
39
|
+
# insert test results into table
|
|
40
|
+
for result in test_results:
|
|
41
|
+
sql_query = f"""
|
|
42
|
+
INSERT INTO p_{self.participant_id}
|
|
43
|
+
VALUES ("{result.test_id}", {result.ability_estimation}, {result.standard_error},
|
|
44
|
+
{result.showed_item}, {result.response}, {result.true_ability_level})"""
|
|
45
|
+
cur.execute(sql_query)
|
|
46
|
+
# commit changes
|
|
47
|
+
con.commit()
|
|
48
|
+
# close connection
|
|
49
|
+
con.close()
|
|
50
|
+
|
|
51
|
+
def load(self) -> List[TestResult]:
|
|
52
|
+
"""Loads results from the database.
|
|
53
|
+
The implementation of this method is required
|
|
54
|
+
by the interface. However, is does not have
|
|
55
|
+
any implemented functionality and will throw an error.
|
|
56
|
+
|
|
57
|
+
Returns: List[TestResult]
|
|
58
|
+
"""
|
|
59
|
+
raise NotImplementedError("This function is not implemented.")
|
|
60
|
+
|
|
61
|
+
def _create_table(self, cur: sqlite3.Cursor) -> None:
|
|
62
|
+
"""Creates a table in the database with the
|
|
63
|
+
name <participant_id> if it does not exist.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
cur (sqlite3.Cursor): database cursor
|
|
67
|
+
"""
|
|
68
|
+
sql_query = f"""CREATE TABLE IF NOT EXISTS p_{self.participant_id} (
|
|
69
|
+
test_id,
|
|
70
|
+
ability_estimation,
|
|
71
|
+
standard_error,
|
|
72
|
+
showed_item,
|
|
73
|
+
response,
|
|
74
|
+
true_ability_level
|
|
75
|
+
)"""
|
|
76
|
+
cur.execute(sql_query)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from ..models.__adaptive_test import AdaptiveTest
|
|
3
|
+
from ..models.__item_pool import ItemPool
|
|
4
|
+
from ..models.__algorithm_exception import AlgorithmException
|
|
5
|
+
from ..math.estimators.__ml_estimation import MLEstimator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DefaultImplementation(AdaptiveTest):
|
|
9
|
+
"""This class represents the Default implementation using
|
|
10
|
+
Maximum Likelihood Estimation and Urry's rule during the test."""
|
|
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=True,
|
|
17
|
+
debug=False):
|
|
18
|
+
"""
|
|
19
|
+
Args:
|
|
20
|
+
item_pool (ItemPool): item pool used for the test
|
|
21
|
+
|
|
22
|
+
simulation_id (str): simulation id
|
|
23
|
+
|
|
24
|
+
participant_id (int): participant id
|
|
25
|
+
|
|
26
|
+
true_ability_level (float): true ability level (must always be set)
|
|
27
|
+
|
|
28
|
+
initial_ability_level (float): initially assumed ability level
|
|
29
|
+
|
|
30
|
+
simulation (bool): will the test be simulated
|
|
31
|
+
|
|
32
|
+
debug (bool): enables debug mode
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
super().__init__(item_pool,
|
|
36
|
+
simulation_id,
|
|
37
|
+
participant_id,
|
|
38
|
+
true_ability_level,
|
|
39
|
+
initial_ability_level,
|
|
40
|
+
simulation,
|
|
41
|
+
debug)
|
|
42
|
+
|
|
43
|
+
def estimate_ability_level(self) -> float:
|
|
44
|
+
"""
|
|
45
|
+
Estimates latent ability level using ML.
|
|
46
|
+
If responses are only 1 or 0,
|
|
47
|
+
the ability will be set to one
|
|
48
|
+
of the boundaries of the estimation interval (`[-10,10]`).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
float: ability estimation
|
|
52
|
+
"""
|
|
53
|
+
estimator = MLEstimator(
|
|
54
|
+
self.response_pattern,
|
|
55
|
+
self.get_answered_items()
|
|
56
|
+
)
|
|
57
|
+
estimation: float = float("NaN")
|
|
58
|
+
try:
|
|
59
|
+
estimation = estimator.get_estimation()
|
|
60
|
+
except AlgorithmException as exception:
|
|
61
|
+
# check if all responses are the same
|
|
62
|
+
if len(set(self.response_pattern)) == 1:
|
|
63
|
+
if self.response_pattern[0] == 0:
|
|
64
|
+
estimation = -10
|
|
65
|
+
elif self.response_pattern[0] == 1:
|
|
66
|
+
estimation = 10
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
raise AlgorithmException("""Something else
|
|
70
|
+
when wrong when running MLE""") from exception
|
|
71
|
+
|
|
72
|
+
return estimation
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
import jax.numpy as np
|
|
3
|
+
from ..models.__test_item import TestItem
|
|
4
|
+
import random
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PreTest:
|
|
8
|
+
def __init__(self, items: List[TestItem], seed: int | None = None):
|
|
9
|
+
"""
|
|
10
|
+
The pretest class can be used to draw items randomly from
|
|
11
|
+
difficulty quantiles
|
|
12
|
+
of the item pool.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
items: Item pool
|
|
16
|
+
|
|
17
|
+
seed (int): A seed for the item selection can be provided.
|
|
18
|
+
If not, the item selection will be drawn randomly, and you will not be able
|
|
19
|
+
to reproduce the results.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
self.items = items
|
|
23
|
+
self.seed = seed
|
|
24
|
+
|
|
25
|
+
def calculate_quantiles(self) -> np.ndarray:
|
|
26
|
+
"""Calculates quantiles 0.25, 0.5, 0.75
|
|
27
|
+
"""
|
|
28
|
+
# get difficulties
|
|
29
|
+
difficulties: List[float] = [item.b for item in self.items]
|
|
30
|
+
|
|
31
|
+
quantiles = np.array([])
|
|
32
|
+
# calculate quantiles
|
|
33
|
+
for q in [0.25, 0.5, 0.75]:
|
|
34
|
+
quantile: np.ndarray = np.quantile(np.array(difficulties), q)
|
|
35
|
+
quantiles = np.append(quantiles, quantile)
|
|
36
|
+
|
|
37
|
+
return quantiles
|
|
38
|
+
|
|
39
|
+
def select_item_in_interval(self, lower: float, upper: float) -> TestItem:
|
|
40
|
+
"""Draws item from a pool in the specified interval.
|
|
41
|
+
The item difficulty is > than the lower limit and <= the higher limit.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
lower (float): Lower bound of the item difficulty interval.
|
|
45
|
+
|
|
46
|
+
upper (float): Upper bound of the item difficulty interval.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
TestItem: Selected item.
|
|
50
|
+
"""
|
|
51
|
+
# select only items with difficulty in interval
|
|
52
|
+
items_in_interval: List[TestItem] = [item for item in list(self.items) if lower < item.b <= upper]
|
|
53
|
+
# draw one item randomly
|
|
54
|
+
if self.seed is not None:
|
|
55
|
+
random.seed(self.seed)
|
|
56
|
+
item = random.sample(items_in_interval, 1)[0]
|
|
57
|
+
return item
|
|
58
|
+
|
|
59
|
+
def select_random_item_quantile(self) -> List[TestItem]:
|
|
60
|
+
"""Selects a random item from the 0.25, 0.5 and 0.75 quantiles.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List[TestItem]: Selected item.
|
|
64
|
+
"""
|
|
65
|
+
selected_items: List[TestItem] = []
|
|
66
|
+
# create difficulties array
|
|
67
|
+
difficulties = np.array([item.b for item in self.items])
|
|
68
|
+
# add minimum and maximum difficulty values to
|
|
69
|
+
# the quantiles list for correct item selection
|
|
70
|
+
quantiles = [difficulties.min()]
|
|
71
|
+
quantiles.extend(list(self.calculate_quantiles()))
|
|
72
|
+
quantiles.extend([difficulties.max()])
|
|
73
|
+
|
|
74
|
+
for i in range(len(quantiles)-1):
|
|
75
|
+
selected_item = self.select_item_in_interval(
|
|
76
|
+
lower=float(quantiles[i]),
|
|
77
|
+
upper=float(quantiles[i+1])
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
selected_items.append(selected_item)
|
|
81
|
+
|
|
82
|
+
# return
|
|
83
|
+
return selected_items
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from ..models.__adaptive_test import AdaptiveTest
|
|
3
|
+
from ..models.__item_pool import ItemPool
|
|
4
|
+
from ..models.__algorithm_exception import AlgorithmException
|
|
5
|
+
from ..models.__test_result import TestResult
|
|
6
|
+
from ..math.estimators.__ml_estimation import MLEstimator
|
|
7
|
+
from .__pre_test import PreTest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SemiAdaptiveImplementation(AdaptiveTest):
|
|
11
|
+
|
|
12
|
+
def __init__(self,
|
|
13
|
+
item_pool: ItemPool,
|
|
14
|
+
simulation_id: str,
|
|
15
|
+
participant_id: int,
|
|
16
|
+
true_ability_level: float,
|
|
17
|
+
initial_ability_level: float = 0,
|
|
18
|
+
simulation=True,
|
|
19
|
+
debug=False,
|
|
20
|
+
pretest_seed=12345):
|
|
21
|
+
"""
|
|
22
|
+
This class represents the Semi-Adaptive implementation using
|
|
23
|
+
Maximum Likelihood Estimation and Urry's rule during the test.
|
|
24
|
+
The pretest is 4 items long.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
item_pool (ItemPool): item pool used for the test
|
|
28
|
+
|
|
29
|
+
simulation_id (str): simulation id
|
|
30
|
+
|
|
31
|
+
participant_id (int): participant id
|
|
32
|
+
|
|
33
|
+
true_ability_level (float): true ability level (must always be set)
|
|
34
|
+
|
|
35
|
+
initial_ability_level (float): initially assumed ability level
|
|
36
|
+
|
|
37
|
+
simulation (bool): will the test be simulated
|
|
38
|
+
|
|
39
|
+
debug (bool): enables debug mode
|
|
40
|
+
|
|
41
|
+
pretest_seed (int): seed used for the random number generator to draw pretest items.
|
|
42
|
+
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
super().__init__(item_pool,
|
|
46
|
+
simulation_id,
|
|
47
|
+
participant_id,
|
|
48
|
+
true_ability_level,
|
|
49
|
+
initial_ability_level,
|
|
50
|
+
simulation,
|
|
51
|
+
debug)
|
|
52
|
+
|
|
53
|
+
self.pretest_seed = pretest_seed
|
|
54
|
+
|
|
55
|
+
def estimate_ability_level(self) -> float:
|
|
56
|
+
"""
|
|
57
|
+
Estimates latent ability level using ML.
|
|
58
|
+
If responses are only 1 or 0,
|
|
59
|
+
the ability will be set to one
|
|
60
|
+
of the boundaries of the estimation interval (`[-10,10]`).
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
float: ability estimation
|
|
64
|
+
"""
|
|
65
|
+
estimator = MLEstimator(
|
|
66
|
+
self.response_pattern,
|
|
67
|
+
self.get_answered_items()
|
|
68
|
+
)
|
|
69
|
+
estimation: float = float("NaN")
|
|
70
|
+
try:
|
|
71
|
+
estimation = estimator.get_estimation()
|
|
72
|
+
except AlgorithmException as exception:
|
|
73
|
+
# check if all responses are the same
|
|
74
|
+
if len(set(self.response_pattern)) == 1:
|
|
75
|
+
if self.response_pattern[0] == 0:
|
|
76
|
+
estimation = -10
|
|
77
|
+
elif self.response_pattern[0] == 1:
|
|
78
|
+
estimation = 10
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
raise AlgorithmException("""Something else
|
|
82
|
+
when wrong when running MLE""") from exception
|
|
83
|
+
|
|
84
|
+
return estimation
|
|
85
|
+
|
|
86
|
+
def pre_test(self):
|
|
87
|
+
"""Runs pretest"""
|
|
88
|
+
# create test instance
|
|
89
|
+
pretest = PreTest(
|
|
90
|
+
items=self.item_pool.test_items,
|
|
91
|
+
seed=self.pretest_seed
|
|
92
|
+
)
|
|
93
|
+
# get selected items
|
|
94
|
+
random_items = pretest.select_random_item_quantile()
|
|
95
|
+
|
|
96
|
+
# get responses
|
|
97
|
+
for item in random_items:
|
|
98
|
+
response = self.item_pool.get_item_response(item)
|
|
99
|
+
# add response to response pattern
|
|
100
|
+
self.response_pattern.append(response)
|
|
101
|
+
# add item to answered items list
|
|
102
|
+
self.answered_items.append(item)
|
|
103
|
+
|
|
104
|
+
# remove random items from item pool
|
|
105
|
+
for item in random_items:
|
|
106
|
+
self.item_pool.delete_item(item)
|
|
107
|
+
|
|
108
|
+
# estimate ability level
|
|
109
|
+
estimation = self.estimate_ability_level()
|
|
110
|
+
self.ability_level = estimation
|
|
111
|
+
|
|
112
|
+
# create test results for all n-1 random items
|
|
113
|
+
for i in range(0, len(random_items) - 1):
|
|
114
|
+
result = TestResult(
|
|
115
|
+
ability_estimation=float("nan"),
|
|
116
|
+
standard_error=float("nan"),
|
|
117
|
+
showed_item=random_items[i].b,
|
|
118
|
+
response=self.response_pattern[i],
|
|
119
|
+
test_id=self.simulation_id,
|
|
120
|
+
true_ability_level=self.true_ability_level,
|
|
121
|
+
)
|
|
122
|
+
# append to memory
|
|
123
|
+
self.test_results.append(result)
|
|
124
|
+
|
|
125
|
+
# create test result for first ability estimation
|
|
126
|
+
intermediate_result = TestResult(
|
|
127
|
+
ability_estimation=self.ability_level,
|
|
128
|
+
standard_error=self.get_ability_se(),
|
|
129
|
+
showed_item=random_items[-1].b,
|
|
130
|
+
response=self.response_pattern[-1],
|
|
131
|
+
test_id=self.simulation_id,
|
|
132
|
+
true_ability_level=self.true_ability_level,
|
|
133
|
+
)
|
|
134
|
+
self.test_results.append(intermediate_result)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import List, Literal
|
|
3
|
+
import jax.numpy as np
|
|
4
|
+
from .__test_information import test_information_function
|
|
5
|
+
from ..models.__test_item import TestItem
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def standard_error(answered_items: List[TestItem],
|
|
9
|
+
estimated_ability_level: float,
|
|
10
|
+
estimator: Literal["ML", "BM"] = "ML",
|
|
11
|
+
sd: float | None = None) -> float:
|
|
12
|
+
"""Calculates the standard error using the test information function.
|
|
13
|
+
If Bayes Modal is used for the ability estimation, a standard deviation value
|
|
14
|
+
of the prior distribution has to be provided.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
answered_items (List[float]): List of answered items
|
|
18
|
+
|
|
19
|
+
estimated_ability_level (float): Currently estimated ability level
|
|
20
|
+
|
|
21
|
+
estimator (Literal["ML", "BM"]): Ability estimator (Default: ML)
|
|
22
|
+
|
|
23
|
+
sd (float | None): Standard deviation of the prior distribution. Only required for BM.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
float: Standard error
|
|
30
|
+
"""
|
|
31
|
+
a=np.array([item.a for item in answered_items])
|
|
32
|
+
b=np.array([item.b for item in answered_items])
|
|
33
|
+
c=np.array([item.c for item in answered_items])
|
|
34
|
+
d=np.array([item.d for item in answered_items])
|
|
35
|
+
|
|
36
|
+
if estimator == "ML":
|
|
37
|
+
error = 1 / math.sqrt(test_information_function(mu=np.array(estimated_ability_level),
|
|
38
|
+
a=a,
|
|
39
|
+
b=b,
|
|
40
|
+
c=c,
|
|
41
|
+
d=d
|
|
42
|
+
))
|
|
43
|
+
|
|
44
|
+
return error
|
|
45
|
+
|
|
46
|
+
if estimator == "BM":
|
|
47
|
+
information = test_information_function(mu=np.array(estimated_ability_level),
|
|
48
|
+
a=a,
|
|
49
|
+
b=b,
|
|
50
|
+
c=c,
|
|
51
|
+
d=d)
|
|
52
|
+
if sd is None:
|
|
53
|
+
raise ValueError("sd cannot be None if BM is used as estimator.")
|
|
54
|
+
|
|
55
|
+
error = 1 / math.sqrt(
|
|
56
|
+
(1 / math.pow(sd, 2)) + information
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
return error
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import jax.numpy as np
|
|
2
|
+
from .estimators.__functions.__estimators import probability_y1, probability_y0
|
|
3
|
+
from jax import jit, grad
|
|
4
|
+
from scipy.differentiate import derivative
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_information_function(
|
|
9
|
+
mu: np.ndarray,
|
|
10
|
+
a: np.ndarray,
|
|
11
|
+
b: np.ndarray,
|
|
12
|
+
c: np.ndarray,
|
|
13
|
+
d: np.ndarray,
|
|
14
|
+
) -> float:
|
|
15
|
+
"""
|
|
16
|
+
Calculates test information.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
mu (np.ndarray): ability level
|
|
20
|
+
a (np.ndarray): discrimination parameter
|
|
21
|
+
b (np.ndarray): difficulty parameter
|
|
22
|
+
c (np.ndarray): guessing parameter
|
|
23
|
+
d (np.ndarray): slipping parameter
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
float: test information
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
p_y1 = probability_y1(mu, a, b, c, d)
|
|
30
|
+
|
|
31
|
+
p_y1_grad = derivative(probability_y1,
|
|
32
|
+
mu,
|
|
33
|
+
args=(a, b, c, d))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
product = (p_y1_grad.df ** 2) / (p_y1 * (1 - p_y1))
|
|
38
|
+
information_tensor = np.cumsum(product)
|
|
39
|
+
# print(f"Information tensor: {information_tensor}")
|
|
40
|
+
information = information_tensor[len(information_tensor) - 1]
|
|
41
|
+
# convert information to float and return
|
|
42
|
+
return float(information)
|
|
43
|
+
|