adaptivetesting 1.1.3__tar.gz → 1.1.5__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.
- adaptivetesting-1.1.5/.python-version +1 -0
- adaptivetesting-1.1.5/CHANGELOG.md +13 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/PKG-INFO +8 -4
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/README.md +3 -0
- adaptivetesting-1.1.5/adaptivetesting/__init__.py +42 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/data/__csv_context.py +19 -8
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/data/__pickle_context.py +14 -8
- adaptivetesting-1.1.5/adaptivetesting/math/estimators/__functions/__bayes.py +56 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__adaptive_test.py +0 -3
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__item_pool.py +18 -2
- adaptivetesting-1.1.5/adaptivetesting/tests/test_descriptives.py +50 -0
- adaptivetesting-1.1.5/adaptivetesting/tests/test_plots.py +97 -0
- adaptivetesting-1.1.5/adaptivetesting/utils/__descriptives.py +63 -0
- adaptivetesting-1.1.5/adaptivetesting/utils/__funcs.py +78 -0
- adaptivetesting-1.1.5/adaptivetesting/utils/__init__.py +13 -0
- adaptivetesting-1.1.5/adaptivetesting/utils/__plots.py +300 -0
- adaptivetesting-1.1.5/docs/source/_static/SPEC-0.svg +1 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/pyproject.toml +5 -7
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/uv.lock +1882 -3022
- adaptivetesting-1.1.3/.python-version +0 -1
- adaptivetesting-1.1.3/CHANGELOG.md +0 -4
- adaptivetesting-1.1.3/adaptivetesting/__init__.py +0 -7
- adaptivetesting-1.1.3/adaptivetesting/math/estimators/__functions/__bayes.py +0 -45
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.github/CONTRIBUTING.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.github/workflows/publish.yml +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.github/workflows/python-test.yml +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.gitignore +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.gitlab-ci.yml +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.readthedocs.yml +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.vscode/extensions.json +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/.vscode/settings.json +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/LICENSE +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/data/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/implementations/__default_implementation.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/implementations/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/implementations/__pre_test.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/implementations/__semi_implementation.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/implementations/__test_assembler.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/__gen_response_pattern.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/estimators/__bayes_modal_estimation.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/estimators/__expect_a_posteriori.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/estimators/__functions/__estimators.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/estimators/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/estimators/__ml_estimation.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/estimators/__prior.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/estimators/__test_information.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/item_selection/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/item_selection/__maximum_information_criterion.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/math/item_selection/__urrys_rule.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__algorithm_exception.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__item_selection_exception.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__misc.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__test_item.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/models/__test_result.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/services/__estimator_interface.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/services/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/services/__item_selection_protocol.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/services/__test_results_interface.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/simulation/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/simulation/__simulation.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/__init__.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_adaptive_test.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_bayes_modal.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_eap.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_generate_response_pattern.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_information_test.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_item_selection.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_load_test_items.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_mle.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_pretest.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_simulation.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_standard_error.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/adaptivetesting/tests/test_test_assembler.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/Makefile +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/data.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/implementations.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/index.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/make.bat +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/math.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/models.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/readme.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/requirements.txt +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/services.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/simulation.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/_static/logo.svg +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/_static/main.css +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/conf.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/data.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/implementations.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/index.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/math.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/models.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/services.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/simulation.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/source/tests.rst +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/docs/tests.md +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/examples/.gitignore +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/examples/simulation.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/examples/testing.py +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/images/default.svg +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/images/semi-adaptive.svg +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/mypy.ini +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/requirements.txt +0 -0
- {adaptivetesting-1.1.3 → adaptivetesting-1.1.5}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Version 1.1.5
|
|
2
|
+
- Bug Fix #46 (numerical stability issue concerning Bayesian ability estimation)
|
|
3
|
+
|
|
4
|
+
# Version 1.1.4
|
|
5
|
+
- Bug Fix #43 (optimization interval in `maximize_posterior` function)
|
|
6
|
+
|
|
7
|
+
# Version 1.1.3
|
|
8
|
+
- Bug Fix #34 (numerical stability issue concerning the ability estimation and standard error calculation)
|
|
9
|
+
|
|
10
|
+
# Version 1.1.2
|
|
11
|
+
|
|
12
|
+
- The `ItemPool` class now correctly honors the `id` field of items. (#30, #31)
|
|
13
|
+
- Examples and the readme file have been updated accordingly.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adaptivetesting
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.5
|
|
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
5
|
Project-URL: Homepage, https://github.com/condecon/adaptivetesting
|
|
6
6
|
Project-URL: Documentation, https://github.com/condecon/adaptivetesting/tree/main/docs
|
|
@@ -9,14 +9,18 @@ Project-URL: Issues, https://github.com/condecon/adaptivetesting/issues
|
|
|
9
9
|
Author-email: Jonas Engicht <dev@condecon.de>
|
|
10
10
|
License-File: LICENSE
|
|
11
11
|
Keywords: computerized-adaptive-testing,item-response-theory,psychology,statistics
|
|
12
|
-
Requires-Python: >=3.
|
|
12
|
+
Requires-Python: >=3.12
|
|
13
|
+
Requires-Dist: matplotlib>=3.9.0
|
|
13
14
|
Requires-Dist: numpy>=2.0.0
|
|
14
15
|
Requires-Dist: pandas>=2.2.0
|
|
15
|
-
Requires-Dist: scipy>=1.
|
|
16
|
-
Requires-Dist: tqdm>=4.
|
|
16
|
+
Requires-Dist: scipy>=1.12.0
|
|
17
|
+
Requires-Dist: tqdm>=4.66.0
|
|
17
18
|
Description-Content-Type: text/markdown
|
|
18
19
|
|
|
19
20
|
# adaptivetesting
|
|
21
|
+
[](https://scientific-python.org/specs/spec-0000/)
|
|
22
|
+
|
|
23
|
+
|
|
20
24
|
|
|
21
25
|
<img src="/docs/source/_static/logo.svg" style="width: 100%">
|
|
22
26
|
</img>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from . import data
|
|
2
|
+
from . import implementations
|
|
3
|
+
from . import math
|
|
4
|
+
from . import models
|
|
5
|
+
from . import services
|
|
6
|
+
from . import simulation
|
|
7
|
+
from . import tests
|
|
8
|
+
|
|
9
|
+
from .data.__csv_context import CSVContext
|
|
10
|
+
from .data.__pickle_context import PickleContext
|
|
11
|
+
|
|
12
|
+
from .implementations.__default_implementation import DefaultImplementation
|
|
13
|
+
from .implementations.__pre_test import PreTest
|
|
14
|
+
from .implementations.__semi_implementation import SemiAdaptiveImplementation
|
|
15
|
+
from .implementations.__test_assembler import TestAssembler
|
|
16
|
+
|
|
17
|
+
from .math.__gen_response_pattern import generate_response_pattern
|
|
18
|
+
from .math.estimators.__ml_estimation import MLEstimator
|
|
19
|
+
from .math.estimators.__bayes_modal_estimation import BayesModal
|
|
20
|
+
from .math.estimators.__expect_a_posteriori import ExpectedAPosteriori
|
|
21
|
+
from .math.estimators.__prior import Prior, NormalPrior, CustomPrior, CustomPriorException
|
|
22
|
+
from .math.estimators.__functions.__estimators import probability_y0, probability_y1, maximize_likelihood_function, likelihood
|
|
23
|
+
from .math.estimators.__functions.__bayes import maximize_posterior
|
|
24
|
+
from .math.estimators.__test_information import test_information_function, item_information_function, prior_information_function
|
|
25
|
+
from .math.item_selection.__maximum_information_criterion import maximum_information_criterion
|
|
26
|
+
from .math.item_selection.__urrys_rule import urrys_rule
|
|
27
|
+
|
|
28
|
+
from .models.__adaptive_test import AdaptiveTest
|
|
29
|
+
from .models.__algorithm_exception import AlgorithmException
|
|
30
|
+
from .models.__item_pool import ItemPool
|
|
31
|
+
from .models.__item_selection_exception import ItemSelectionException
|
|
32
|
+
from .models.__test_item import TestItem
|
|
33
|
+
from .models.__test_result import TestResult
|
|
34
|
+
from .models.__misc import ResultOutputFormat, StoppingCriterion
|
|
35
|
+
|
|
36
|
+
from .services.__estimator_interface import IEstimator
|
|
37
|
+
from .services.__test_results_interface import ITestResults
|
|
38
|
+
from .services.__item_selection_protocol import ItemSelectionStrategy
|
|
39
|
+
|
|
40
|
+
from .simulation.__simulation import Simulation, SimulationPool, setup_simulation_and_start
|
|
41
|
+
|
|
42
|
+
from .utils.__descriptives import bias, average_absolute_deviation, rmse
|
|
@@ -3,6 +3,7 @@ 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
7
|
|
|
7
8
|
|
|
8
9
|
class CSVContext(ITestResults):
|
|
@@ -46,12 +47,22 @@ class CSVContext(ITestResults):
|
|
|
46
47
|
file.close()
|
|
47
48
|
|
|
48
49
|
def load(self) -> List[TestResult]:
|
|
49
|
-
"""Loads results from
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
56
57
|
"""
|
|
57
|
-
|
|
58
|
+
foldername = f"data/{self.simulation_id}"
|
|
59
|
+
|
|
60
|
+
fieldnames = list(fields(TestResult))
|
|
61
|
+
test_results: list[TestResult] = []
|
|
62
|
+
with open(f"{foldername}/{self.participant_id}.csv", "r", encoding="utf-8") as file:
|
|
63
|
+
reader = csv.DictReader(file, fieldnames=fieldnames)
|
|
64
|
+
for row in reader:
|
|
65
|
+
test_result = TestResult.from_dict(row)
|
|
66
|
+
test_results.append(test_result)
|
|
67
|
+
file.close()
|
|
68
|
+
return test_results
|
|
@@ -40,12 +40,18 @@ class PickleContext(ITestResults):
|
|
|
40
40
|
file.close()
|
|
41
41
|
|
|
42
42
|
def load(self) -> List[TestResult]:
|
|
43
|
-
"""Loads
|
|
44
|
-
The
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
"""Loads and returns a list of TestResult objects for a specific participant and simulation.
|
|
44
|
+
The method reads a pickle file located at 'data/{simulation_id}/{participant_id}.pickle'
|
|
45
|
+
and deserializes its contents into a list of TestResult instances.
|
|
46
|
+
Returns:
|
|
47
|
+
List[TestResult]: The list of test results loaded from the pickle file.
|
|
48
|
+
Raises:
|
|
49
|
+
FileNotFoundError: If the specified pickle file does not exist.
|
|
50
|
+
pickle.UnpicklingError: If the file cannot be unpickled.
|
|
50
51
|
"""
|
|
51
|
-
|
|
52
|
+
foldername = f"data/{self.simulation_id}"
|
|
53
|
+
|
|
54
|
+
with open(f"{foldername}/{self.participant_id}.pickle", "rb") as file:
|
|
55
|
+
test_results: list[TestResult] = pickle.load(file)
|
|
56
|
+
file.close()
|
|
57
|
+
return test_results
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy.optimize import minimize_scalar, OptimizeResult # type: ignore
|
|
3
|
+
from .__estimators import likelihood
|
|
4
|
+
from ..__prior import Prior
|
|
5
|
+
from ....models.__algorithm_exception import AlgorithmException
|
|
6
|
+
from .__estimators import probability_y0, probability_y1
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def maximize_posterior(
|
|
10
|
+
a: np.ndarray,
|
|
11
|
+
b: np.ndarray,
|
|
12
|
+
c: np.ndarray,
|
|
13
|
+
d: np.ndarray,
|
|
14
|
+
response_pattern: np.ndarray,
|
|
15
|
+
prior: Prior,
|
|
16
|
+
optimization_interval: tuple[float, float] = (-10, 10)
|
|
17
|
+
) -> float:
|
|
18
|
+
"""_summary_
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
a (np.ndarray): item parameter a
|
|
22
|
+
|
|
23
|
+
b (np.ndarray): item parameter b
|
|
24
|
+
|
|
25
|
+
c (np.ndarray): item parameter c
|
|
26
|
+
|
|
27
|
+
d (np.ndarray): item parameter d
|
|
28
|
+
|
|
29
|
+
response_pattern (np.ndarray): response pattern (simulated or user generated)
|
|
30
|
+
|
|
31
|
+
prior (Prior): prior distribution
|
|
32
|
+
|
|
33
|
+
optimization_interval (Tuple[float, float]): interval used for the optimization function
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
float: Bayes Modal estimator for the given parameters
|
|
37
|
+
"""
|
|
38
|
+
def log_posterior(mu) -> np.ndarray:
|
|
39
|
+
p1 = probability_y1(mu, a, b, c, d)
|
|
40
|
+
p0 = probability_y0(mu, a, b, c, d)
|
|
41
|
+
|
|
42
|
+
log_likelihood = np.sum((response_pattern * np.log(p1 + 1e-300)) + \
|
|
43
|
+
((1 - response_pattern) * np.log(p0 + 1e-300)))
|
|
44
|
+
log_prior = np.log(prior.pdf(mu) + 1e-300)
|
|
45
|
+
|
|
46
|
+
return log_likelihood + log_prior
|
|
47
|
+
|
|
48
|
+
result: OptimizeResult = minimize_scalar(lambda mu: -log_posterior(mu),
|
|
49
|
+
bounds=optimization_interval,
|
|
50
|
+
method="bounded") # type: ignore
|
|
51
|
+
|
|
52
|
+
if not result.success:
|
|
53
|
+
raise AlgorithmException(f"Optimization failed: {result.message}")
|
|
54
|
+
|
|
55
|
+
else:
|
|
56
|
+
return float(result.x)
|
|
@@ -2,7 +2,6 @@ from typing import List
|
|
|
2
2
|
import abc
|
|
3
3
|
import copy
|
|
4
4
|
from .__test_item import TestItem
|
|
5
|
-
from ..math.item_selection.__urrys_rule import urrys_rule
|
|
6
5
|
from ..math.__gen_response_pattern import generate_response_pattern
|
|
7
6
|
from .__test_result import TestResult
|
|
8
7
|
from .__item_pool import ItemPool
|
|
@@ -114,8 +113,6 @@ class AdaptiveTest(abc.ABC):
|
|
|
114
113
|
TestItem: selected item
|
|
115
114
|
"""
|
|
116
115
|
raise NotImplementedError("This functionality is not implemented by default.")
|
|
117
|
-
item = urrys_rule(self.item_pool.test_items, self.ability_level)
|
|
118
|
-
return item
|
|
119
116
|
|
|
120
117
|
@abc.abstractmethod
|
|
121
118
|
def estimate_ability_level(self) -> tuple[float, float]:
|
|
@@ -110,7 +110,9 @@ class ItemPool:
|
|
|
110
110
|
|
|
111
111
|
simulated_responses (List[int]): simulated responses
|
|
112
112
|
|
|
113
|
-
ids (List[int]): item IDs
|
|
113
|
+
ids (List[int]): item IDs. If the argument is set to `None`
|
|
114
|
+
all items are numbered in the order in which they are
|
|
115
|
+
passed to the function.
|
|
114
116
|
|
|
115
117
|
Returns:
|
|
116
118
|
List[TestItem]: item pool
|
|
@@ -147,6 +149,9 @@ class ItemPool:
|
|
|
147
149
|
raise ValueError("Length of ids and b has to be the same.")
|
|
148
150
|
for i, id_ in enumerate(ids):
|
|
149
151
|
items[i].id = id_
|
|
152
|
+
else:
|
|
153
|
+
for i in range(len(b)):
|
|
154
|
+
items[i].id = i
|
|
150
155
|
|
|
151
156
|
item_pool = ItemPool(items)
|
|
152
157
|
item_pool.simulated_responses = simulated_responses
|
|
@@ -169,7 +174,9 @@ class ItemPool:
|
|
|
169
174
|
Args:
|
|
170
175
|
source (dict[str, List[float]]): item pool dictionary
|
|
171
176
|
simulated_responses (List[int]): simulated responses
|
|
172
|
-
ids (List[int]): item IDs
|
|
177
|
+
ids (List[int]): item IDs. If the argument is set to `None`
|
|
178
|
+
all items are numbered in the order in which they are
|
|
179
|
+
passed to the function.
|
|
173
180
|
|
|
174
181
|
Returns:
|
|
175
182
|
List[TestItem]: item pool
|
|
@@ -211,6 +218,8 @@ class ItemPool:
|
|
|
211
218
|
|
|
212
219
|
if ids is not None:
|
|
213
220
|
item.id = ids[i]
|
|
221
|
+
else:
|
|
222
|
+
item.id = i
|
|
214
223
|
|
|
215
224
|
items.append(item)
|
|
216
225
|
|
|
@@ -223,6 +232,13 @@ class ItemPool:
|
|
|
223
232
|
"""Creates item pool from a pandas DataFrame.
|
|
224
233
|
Required columns are: `a`, `b`, `c`, `d`.
|
|
225
234
|
Each column has to contain float values.
|
|
235
|
+
|
|
236
|
+
A `id` column can be added to assign
|
|
237
|
+
each test item a unique identifier.
|
|
238
|
+
If there is no `id` column
|
|
239
|
+
all items are numbered in the order in which they are
|
|
240
|
+
passed to the function.
|
|
241
|
+
|
|
226
242
|
A `simulated_responses` (int values) column can be added to
|
|
227
243
|
the DataFrame to provide simulated responses.
|
|
228
244
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
import numpy as np
|
|
4
|
+
from adaptivetesting import rmse, bias, average_absolute_deviation, TestResult
|
|
5
|
+
from adaptivetesting.models.__misc import ResultOutputFormat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DummyResult:
|
|
9
|
+
def __init__(self, ability_estimation, true_ability_level):
|
|
10
|
+
self.ability_estimation = ability_estimation
|
|
11
|
+
self.true_ability_level = true_ability_level
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestDescriptives(unittest.TestCase):
|
|
15
|
+
@patch('adaptivetesting.utils.__descriptives.load_final_test_results')
|
|
16
|
+
def test_bias(self, mock_load):
|
|
17
|
+
mock_load.return_value = [
|
|
18
|
+
TestResult(None, 1.0, None, None, None, 0.5), # type: ignore
|
|
19
|
+
TestResult(None, 2.0, None, None, None, 1.5), # type: ignore
|
|
20
|
+
TestResult(None, 3.0, None, None, None, 2.5), # type: ignore
|
|
21
|
+
]
|
|
22
|
+
result = bias('sim1', ['p1', 'p2', 'p3'], ResultOutputFormat.CSV)
|
|
23
|
+
expected = np.mean([1.0 - 0.5, 2.0 - 1.5, 3.0 - 2.5])
|
|
24
|
+
self.assertAlmostEqual(result, expected) # type: ignore
|
|
25
|
+
|
|
26
|
+
@patch('adaptivetesting.utils.__descriptives.load_final_test_results')
|
|
27
|
+
def test_average_absolute_deviation(self, mock_load):
|
|
28
|
+
mock_load.return_value = [
|
|
29
|
+
TestResult(None, 1.0, None, None, None, 0.5), # type: ignore
|
|
30
|
+
TestResult(None, 2.0, None, None, None, 1.5), # type: ignore
|
|
31
|
+
TestResult(None, 3.0, None, None, None, 2.5), # type: ignore
|
|
32
|
+
]
|
|
33
|
+
result = average_absolute_deviation('sim1', ['p1', 'p2', 'p3'], ResultOutputFormat.CSV)
|
|
34
|
+
expected = np.mean([abs(1.0 - 0.5), abs(2.0 - 1.5), abs(3.0 - 2.5)])
|
|
35
|
+
self.assertAlmostEqual(result, expected) # type: ignore
|
|
36
|
+
|
|
37
|
+
@patch('adaptivetesting.utils.__descriptives.load_final_test_results')
|
|
38
|
+
def test_rmse(self, mock_load):
|
|
39
|
+
mock_load.return_value = [
|
|
40
|
+
TestResult(None, 1.0, None, None, None, 0.5), # type: ignore
|
|
41
|
+
TestResult(None, 2.0, None, None, None, 1.5), # type: ignore
|
|
42
|
+
TestResult(None, 3.0, None, None, None, 2.5), # type: ignore
|
|
43
|
+
]
|
|
44
|
+
result = rmse('sim1', ['p1', 'p2', 'p3'], ResultOutputFormat.CSV)
|
|
45
|
+
expected = np.sqrt(np.mean([(1.0 - 0.5) ** 2, (2.0 - 1.5) ** 2, (3.0 - 2.5) ** 2]))
|
|
46
|
+
self.assertAlmostEqual(result, expected)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == '__main__':
|
|
50
|
+
unittest.main()
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
from adaptivetesting.utils import (
|
|
5
|
+
plot_final_ability_estimates,
|
|
6
|
+
plot_icc,
|
|
7
|
+
plot_iif,
|
|
8
|
+
plot_exposure_rate,
|
|
9
|
+
plot_test_information,
|
|
10
|
+
plot_theta_estimation_trace
|
|
11
|
+
)
|
|
12
|
+
from adaptivetesting.models.__test_item import TestItem
|
|
13
|
+
from adaptivetesting.models.__misc import ResultOutputFormat
|
|
14
|
+
import matplotlib
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestPlots(unittest.TestCase):
|
|
18
|
+
@classmethod
|
|
19
|
+
def setUpClass(cls):
|
|
20
|
+
cls.dummy_item = TestItem()
|
|
21
|
+
cls.dummy_item.id = 1
|
|
22
|
+
cls.dummy_item.a = 1.0
|
|
23
|
+
cls.dummy_item.b = 0.0
|
|
24
|
+
cls.dummy_item.c = 0.2
|
|
25
|
+
cls.dummy_item.d = 1.0
|
|
26
|
+
cls.dummy_items = [cls.dummy_item for _ in range(3)]
|
|
27
|
+
cls.simulation_id = 'sim1'
|
|
28
|
+
cls.participant_ids = ['p1', 'p2']
|
|
29
|
+
cls.output_format = ResultOutputFormat.CSV
|
|
30
|
+
|
|
31
|
+
@patch('adaptivetesting.utils.__plots.load_final_test_results')
|
|
32
|
+
def test_plot_final_ability_estimates(self, mock_load):
|
|
33
|
+
# Mock return value: list of objects with .ability_estimation and .true_ability_level
|
|
34
|
+
class DummyResult:
|
|
35
|
+
def __init__(self, est, true):
|
|
36
|
+
self.ability_estimation = est
|
|
37
|
+
self.true_ability_level = true
|
|
38
|
+
mock_load.return_value = [DummyResult(1.0, 1.2), DummyResult(0.5, 0.7)]
|
|
39
|
+
fig, ax = plot_final_ability_estimates(
|
|
40
|
+
self.simulation_id,
|
|
41
|
+
self.participant_ids,
|
|
42
|
+
self.output_format
|
|
43
|
+
)
|
|
44
|
+
self.assertIsInstance(fig, matplotlib.figure.Figure)
|
|
45
|
+
self.assertIsNotNone(ax)
|
|
46
|
+
|
|
47
|
+
def test_plot_icc(self):
|
|
48
|
+
fig, ax = plot_icc(self.dummy_item)
|
|
49
|
+
self.assertIsInstance(fig, matplotlib.figure.Figure)
|
|
50
|
+
self.assertIsNotNone(ax)
|
|
51
|
+
|
|
52
|
+
def test_plot_iif(self):
|
|
53
|
+
fig, ax = plot_iif(self.dummy_item)
|
|
54
|
+
self.assertIsInstance(fig, matplotlib.figure.Figure)
|
|
55
|
+
self.assertIsNotNone(ax)
|
|
56
|
+
|
|
57
|
+
@patch('adaptivetesting.utils.__plots.load_test_results_single_participant')
|
|
58
|
+
def test_plot_exposure_rate(self, mock_load):
|
|
59
|
+
# Mock return value: list of objects with .showed_item["id"]
|
|
60
|
+
class DummyResult:
|
|
61
|
+
def __init__(self, item_id):
|
|
62
|
+
self.showed_item = {"id": item_id}
|
|
63
|
+
mock_load.return_value = [
|
|
64
|
+
DummyResult("item1"), DummyResult("item2")
|
|
65
|
+
]
|
|
66
|
+
fig, ax = plot_exposure_rate(
|
|
67
|
+
self.simulation_id,
|
|
68
|
+
self.participant_ids,
|
|
69
|
+
self.output_format
|
|
70
|
+
)
|
|
71
|
+
self.assertIsInstance(fig, matplotlib.figure.Figure)
|
|
72
|
+
self.assertIsNotNone(ax)
|
|
73
|
+
|
|
74
|
+
def test_plot_test_information(self):
|
|
75
|
+
fig, ax = plot_test_information(self.dummy_items)
|
|
76
|
+
self.assertIsInstance(fig, matplotlib.figure.Figure)
|
|
77
|
+
self.assertIsNotNone(ax)
|
|
78
|
+
|
|
79
|
+
@patch('adaptivetesting.utils.__plots.load_test_results_single_participant')
|
|
80
|
+
def test_plot_theta_estimation_trace(self, mock_load):
|
|
81
|
+
# Mock return value: list of objects with .true_ability_level and .ability_estimation
|
|
82
|
+
class DummyResult:
|
|
83
|
+
def __init__(self, true, est):
|
|
84
|
+
self.true_ability_level = true
|
|
85
|
+
self.ability_estimation = est
|
|
86
|
+
mock_load.return_value = [DummyResult(1.0, 0.9), DummyResult(1.2, 1.1)]
|
|
87
|
+
fig, ax = plot_theta_estimation_trace(
|
|
88
|
+
self.simulation_id,
|
|
89
|
+
self.participant_ids[0],
|
|
90
|
+
self.output_format
|
|
91
|
+
)
|
|
92
|
+
self.assertIsInstance(fig, matplotlib.figure.Figure)
|
|
93
|
+
self.assertIsNotNone(ax)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
unittest.main()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from ..models.__misc import ResultOutputFormat
|
|
2
|
+
import numpy as np
|
|
3
|
+
from .__funcs import load_final_test_results
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def bias(simulation_id: str, participant_ids: list[str], output_format: ResultOutputFormat) -> np.floating:
|
|
7
|
+
"""
|
|
8
|
+
Calculates the bias of ability estimations for a set of participants in a simulation.
|
|
9
|
+
Bias is defined as the mean difference between estimated abilities and true ability levels.
|
|
10
|
+
Args:
|
|
11
|
+
simulation_id (str): Identifier for the simulation.
|
|
12
|
+
participant_ids (list[str]): List of participant identifiers.
|
|
13
|
+
output_format (ResultOutputFormat): Format in which the results are output.
|
|
14
|
+
Returns:
|
|
15
|
+
np.floating: The mean bias of ability estimations.
|
|
16
|
+
"""
|
|
17
|
+
final_test_results = load_final_test_results(simulation_id, participant_ids, output_format)
|
|
18
|
+
estimations = np.array([result.ability_estimation for result in final_test_results], dtype=np.float64)
|
|
19
|
+
true_abilities = np.array([result.true_ability_level for result in final_test_results], dtype=np.float64)
|
|
20
|
+
|
|
21
|
+
return np.mean(estimations - true_abilities)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def average_absolute_deviation(simulation_id: str,
|
|
25
|
+
participant_ids: list[str],
|
|
26
|
+
output_format: ResultOutputFormat) -> np.floating:
|
|
27
|
+
"""
|
|
28
|
+
Calculates the average absolute deviation between estimated abilities
|
|
29
|
+
and true ability levels for a set of participants.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
simulation_id (str): Identifier for the simulation run.
|
|
33
|
+
participant_ids (list[str]): List of participant identifiers.
|
|
34
|
+
output_format (ResultOutputFormat): Format in which the results are returned.
|
|
35
|
+
Returns:
|
|
36
|
+
np.floating: The mean absolute deviation between estimated and true abilities.
|
|
37
|
+
"""
|
|
38
|
+
final_test_results = load_final_test_results(simulation_id, participant_ids, output_format)
|
|
39
|
+
estimations = np.array([result.ability_estimation for result in final_test_results], dtype=np.float64)
|
|
40
|
+
true_abilities = np.array([result.true_ability_level for result in final_test_results], dtype=np.float64)
|
|
41
|
+
|
|
42
|
+
return np.mean(np.abs(estimations - true_abilities))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rmse(simulation_id: str,
|
|
46
|
+
participant_ids: list[str],
|
|
47
|
+
output_format: ResultOutputFormat) -> np.floating:
|
|
48
|
+
"""
|
|
49
|
+
Calculates the root mean squared error (RMSE) between
|
|
50
|
+
estimated abilities and true ability levels for a set of participants.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
simulation_id (str): Identifier for the simulation run.
|
|
54
|
+
participant_ids (list[str]): List of participant IDs to include in the calculation.
|
|
55
|
+
output_format (ResultOutputFormat): Format in which the test results are returned.
|
|
56
|
+
Returns:
|
|
57
|
+
np.floating: The RMSE value between estimated and true abilities.
|
|
58
|
+
"""
|
|
59
|
+
final_test_results = load_final_test_results(simulation_id, participant_ids, output_format)
|
|
60
|
+
estimations = np.array([result.ability_estimation for result in final_test_results], dtype=np.float64)
|
|
61
|
+
true_abilities = np.array([result.true_ability_level for result in final_test_results], dtype=np.float64)
|
|
62
|
+
|
|
63
|
+
return np.sqrt(np.mean((estimations - true_abilities) ** 2))
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from ..models.__test_result import TestResult
|
|
2
|
+
from ..data.__csv_context import CSVContext
|
|
3
|
+
from ..data.__pickle_context import PickleContext
|
|
4
|
+
from ..services.__test_results_interface import ITestResults
|
|
5
|
+
from ..models.__misc import ResultOutputFormat
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_final_test_results(simulation_id: str,
|
|
9
|
+
participant_ids: list[str],
|
|
10
|
+
output_format: ResultOutputFormat) -> list[TestResult]:
|
|
11
|
+
"""
|
|
12
|
+
Loads the final test results for a list of participants from the specified simulation or adaptive test.
|
|
13
|
+
Depending on the output format (CSV or PICKLE), this function initializes the appropriate context,
|
|
14
|
+
loads all test results for each participant, and selects the final result (assumed to be the last entry).
|
|
15
|
+
The final results are collected and returned as a list.
|
|
16
|
+
Args:
|
|
17
|
+
simulation_id (str): The identifier for the simulation from which to load results.
|
|
18
|
+
participant_ids (list[str]): A list of participant IDs whose results are to be loaded.
|
|
19
|
+
output_format (ResultOutputFormat): The format in which results are stored (CSV or PICKLE).
|
|
20
|
+
Returns:
|
|
21
|
+
list[TestResult]: A list containing the final test result for each participant.
|
|
22
|
+
Raises:
|
|
23
|
+
ValueError: If the output_format is not set to either ResultOutputFormat.CSV or ResultOutputFormat.PICKLE.
|
|
24
|
+
"""
|
|
25
|
+
# load data
|
|
26
|
+
final_test_results: list[TestResult] = []
|
|
27
|
+
context: ITestResults
|
|
28
|
+
|
|
29
|
+
if output_format is ResultOutputFormat.CSV:
|
|
30
|
+
for id in participant_ids:
|
|
31
|
+
context = CSVContext(simulation_id, participant_id=id)
|
|
32
|
+
test_results = context.load()
|
|
33
|
+
# select final result
|
|
34
|
+
final_result = test_results[-1]
|
|
35
|
+
final_test_results.append(final_result)
|
|
36
|
+
|
|
37
|
+
if output_format is ResultOutputFormat.PICKLE:
|
|
38
|
+
for id in participant_ids:
|
|
39
|
+
context = PickleContext(simulation_id, participant_id=id)
|
|
40
|
+
test_results = context.load()
|
|
41
|
+
# select final result
|
|
42
|
+
final_result = test_results[-1]
|
|
43
|
+
final_test_results.append(final_result)
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("output_format is not correctly set to either PICKLE of CSV.")
|
|
46
|
+
|
|
47
|
+
return final_test_results
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_test_results_single_participant(simulation_id: str,
|
|
51
|
+
participant_id: str,
|
|
52
|
+
output_format: ResultOutputFormat) -> list[TestResult]:
|
|
53
|
+
"""
|
|
54
|
+
Loads the test results for a participant from the specified simulation or adaptive test.
|
|
55
|
+
Depending on the output format (CSV or PICKLE), this function reads the available CSV or PICKLE files.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
simulation_id (str): The identifier for the simulation from which to load results.
|
|
59
|
+
participant_id (str): Participant ID whose results are to be loaded.
|
|
60
|
+
output_format (ResultOutputFormat): The format in which results are stored (CSV or PICKLE).
|
|
61
|
+
Returns:
|
|
62
|
+
list[TestResult]: A list containing the test results for the participant.
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If the output_format is not set to either ResultOutputFormat.CSV or ResultOutputFormat.PICKLE.
|
|
65
|
+
"""
|
|
66
|
+
context: ITestResults
|
|
67
|
+
|
|
68
|
+
if output_format is ResultOutputFormat.CSV:
|
|
69
|
+
context = CSVContext(simulation_id=simulation_id,
|
|
70
|
+
participant_id=participant_id)
|
|
71
|
+
return context.load()
|
|
72
|
+
|
|
73
|
+
if output_format is ResultOutputFormat.PICKLE:
|
|
74
|
+
context = PickleContext(simulation_id=simulation_id,
|
|
75
|
+
participant_id=participant_id)
|
|
76
|
+
return context.load()
|
|
77
|
+
else:
|
|
78
|
+
raise ValueError("output_format is not correctly set to either PICKLE of CSV.")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .__descriptives import bias, average_absolute_deviation, rmse
|
|
2
|
+
|
|
3
|
+
from .__funcs import load_final_test_results, load_test_results_single_participant
|
|
4
|
+
|
|
5
|
+
from .__plots import (
|
|
6
|
+
plot_exposure_rate,
|
|
7
|
+
plot_final_ability_estimates,
|
|
8
|
+
plot_icc,
|
|
9
|
+
plot_iif,
|
|
10
|
+
plot_exposure_rate,
|
|
11
|
+
plot_test_information,
|
|
12
|
+
plot_theta_estimation_trace
|
|
13
|
+
)
|