adaptivetesting 1.1.1__tar.gz → 1.1.3__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.3/.github/CONTRIBUTING.md +11 -0
- adaptivetesting-1.1.3/.readthedocs.yml +22 -0
- adaptivetesting-1.1.3/CHANGELOG.md +4 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/PKG-INFO +43 -20
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/README.md +42 -19
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__functions/__estimators.py +49 -21
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__test_information.py +8 -18
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/models/__item_pool.py +48 -24
- adaptivetesting-1.1.3/adaptivetesting/models/__test_item.py +33 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_load_test_items.py +65 -1
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_test_assembler.py +1 -1
- adaptivetesting-1.1.3/docs/requirements.txt +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/conf.py +5 -3
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/examples/testing.py +7 -4
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/pyproject.toml +6 -7
- adaptivetesting-1.1.3/requirements.txt +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/uv.lock +2268 -2195
- adaptivetesting-1.1.1/BREAKING_CHANGES.md +0 -65
- adaptivetesting-1.1.1/adaptivetesting/models/__test_item.py +0 -25
- adaptivetesting-1.1.1/requirements.txt +0 -612
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/.github/workflows/publish.yml +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/.github/workflows/python-test.yml +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/.gitignore +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/.gitlab-ci.yml +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/.python-version +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/.vscode/extensions.json +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/.vscode/settings.json +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/LICENSE +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/data/__csv_context.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/data/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/data/__pickle_context.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/implementations/__default_implementation.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/implementations/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/implementations/__pre_test.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/implementations/__semi_implementation.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/implementations/__test_assembler.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/__gen_response_pattern.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__bayes_modal_estimation.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__expect_a_posteriori.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__functions/__bayes.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__ml_estimation.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/estimators/__prior.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/item_selection/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/item_selection/__maximum_information_criterion.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/math/item_selection/__urrys_rule.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/models/__adaptive_test.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/models/__algorithm_exception.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/models/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/models/__item_selection_exception.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/models/__misc.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/models/__test_result.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/services/__estimator_interface.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/services/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/services/__item_selection_protocol.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/services/__test_results_interface.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/simulation/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/simulation/__simulation.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/__init__.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_adaptive_test.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_bayes_modal.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_eap.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_generate_response_pattern.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_information_test.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_item_selection.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_mle.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_pretest.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_simulation.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/adaptivetesting/tests/test_standard_error.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/Makefile +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/data.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/implementations.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/index.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/make.bat +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/math.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/models.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/readme.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/services.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/simulation.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/_static/logo.svg +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/_static/main.css +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/data.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/implementations.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/index.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/math.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/models.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/services.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/simulation.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/source/tests.rst +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/docs/tests.md +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/examples/.gitignore +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/examples/simulation.py +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/images/default.svg +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/images/semi-adaptive.svg +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/mypy.ini +0 -0
- {adaptivetesting-1.1.1 → adaptivetesting-1.1.3}/setup.cfg +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Contributing Guidelines
|
|
2
|
+
|
|
3
|
+
Thank you for your interest in contributing!
|
|
4
|
+
|
|
5
|
+
- All pull requests **must** be opened against the `dev` branch.
|
|
6
|
+
- Ensure your code passes all test workflows:
|
|
7
|
+
- `mypy` (type checking)
|
|
8
|
+
- `flake8` (linting)
|
|
9
|
+
- Unit tests
|
|
10
|
+
|
|
11
|
+
Pull requests that meet these requirements will be considered for merging.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Read the Docs configuration file
|
|
2
|
+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
|
3
|
+
|
|
4
|
+
# Required
|
|
5
|
+
version: 2
|
|
6
|
+
|
|
7
|
+
# Set the OS, Python version, and other tools you might need
|
|
8
|
+
build:
|
|
9
|
+
os: ubuntu-24.04
|
|
10
|
+
tools:
|
|
11
|
+
python: "3.13"
|
|
12
|
+
|
|
13
|
+
# Build documentation in the "docs/" directory with Sphinx
|
|
14
|
+
sphinx:
|
|
15
|
+
configuration: docs/source/conf.py
|
|
16
|
+
|
|
17
|
+
python:
|
|
18
|
+
install:
|
|
19
|
+
- method: pip
|
|
20
|
+
path: .
|
|
21
|
+
- requirements: docs/requirements.txt
|
|
22
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: adaptivetesting
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
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
|
|
@@ -29,7 +29,7 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
- **Bayesian Methods**: Built-in support for Bayesian ability estimation with customizable priors
|
|
30
30
|
- **Flexible Architecture**: Object-oriented design with abstract classes for easy extension
|
|
31
31
|
- **Item Response Theory**: Full support for 1PL, 2PL, 3PL, and 4PL models
|
|
32
|
-
- **Multiple Estimators**:
|
|
32
|
+
- **Multiple Estimators**:
|
|
33
33
|
- Maximum Likelihood Estimation (MLE)
|
|
34
34
|
- Bayesian Modal Estimation (BM)
|
|
35
35
|
- Expected A Posteriori (EAP)
|
|
@@ -131,25 +131,31 @@ adaptive_test = TestAssembler(
|
|
|
131
131
|
### Real-world Testing (Non-simulation) with PsychoPy
|
|
132
132
|
|
|
133
133
|
```python
|
|
134
|
+
# setup item pool
|
|
135
|
+
# the item pool is retrieved from the PREVIC
|
|
136
|
+
# https://github.com/manuelbohn/previc/tree/main/saves
|
|
137
|
+
import pandas as pd
|
|
138
|
+
from adaptivetesting.models import ItemPool
|
|
134
139
|
from psychopy import visual, event
|
|
135
140
|
from psychopy.hardware import keyboard
|
|
136
141
|
from adaptivetesting.implementations import TestAssembler
|
|
137
142
|
from adaptivetesting.models import AdaptiveTest, ItemPool, TestItem
|
|
138
143
|
from adaptivetesting.data import CSVContext
|
|
139
|
-
from adaptivetesting.math.estimators import
|
|
144
|
+
from adaptivetesting.math.estimators import ExpectedAPosteriori, CustomPrior
|
|
140
145
|
from adaptivetesting.math.item_selection import maximum_information_criterion
|
|
141
146
|
from scipy.stats import t
|
|
142
147
|
import pandas as pd
|
|
143
148
|
|
|
149
|
+
previc_item_pool = pd.read_csv("item_pool.csv")
|
|
150
|
+
# add item column
|
|
151
|
+
previc_item_pool["id"] = list(range(1, 90))
|
|
152
|
+
previc_item_pool.head()
|
|
144
153
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
|
|
150
|
-
"d": [0.87, 0.93, 1.0, 0.89, 0.94] # upper asymptote
|
|
151
|
-
})
|
|
152
|
-
item_pool = ItemPool.load_from_dataframe(items_data)
|
|
154
|
+
|
|
155
|
+
item_pool = ItemPool.load_from_list(
|
|
156
|
+
b=previc_item_pool["Difficulty"],
|
|
157
|
+
ids=previc_item_pool["id"]
|
|
158
|
+
)
|
|
153
159
|
|
|
154
160
|
# Create adaptive test
|
|
155
161
|
adaptive_test: AdaptiveTest = TestAssembler(
|
|
@@ -179,12 +185,12 @@ win = visual.Window([800, 600],
|
|
|
179
185
|
# init keyboard
|
|
180
186
|
keyboard.Keyboard()
|
|
181
187
|
|
|
188
|
+
## FIX THIS
|
|
182
189
|
|
|
183
190
|
# define function to get user input
|
|
184
191
|
def get_response(item: TestItem) -> int:
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
stimuli: str = [item for item in item_pool.test_items if item["Difficulty"] == item_difficulty][0]["word"]
|
|
192
|
+
# select corresponding word from item pool data frame
|
|
193
|
+
stimuli: str = previc_item_pool[previc_item_pool["id"] == item.id]["word"].values[0]
|
|
188
194
|
|
|
189
195
|
# create text box and display stimulus
|
|
190
196
|
text_box = visual.TextBox2(win=win,
|
|
@@ -309,17 +315,18 @@ adaptive_test = TestAssembler(
|
|
|
309
315
|
Full documentation is available in the `docs/` directory:
|
|
310
316
|
|
|
311
317
|
- [API Reference](docs/readme.md)
|
|
312
|
-
- [Models Module](docs/
|
|
313
|
-
- [Math Module](docs/
|
|
314
|
-
- [Implementation Examples](docs/
|
|
315
|
-
- [Simulation Guide](docs/
|
|
318
|
+
- [Models Module](docs/models.md)
|
|
319
|
+
- [Math Module](docs/math.md)
|
|
320
|
+
- [Implementation Examples](docs/implementations.md)
|
|
321
|
+
- [Simulation Guide](docs/simulation.md)
|
|
316
322
|
|
|
317
323
|
## Testing
|
|
318
324
|
|
|
319
325
|
The package includes comprehensive tests. Run them using:
|
|
320
326
|
|
|
321
327
|
```bash
|
|
322
|
-
|
|
328
|
+
uv sync
|
|
329
|
+
uv run python -m unittest
|
|
323
330
|
```
|
|
324
331
|
|
|
325
332
|
## Contributing
|
|
@@ -327,7 +334,7 @@ python -m pytest adaptivetesting/tests/
|
|
|
327
334
|
We welcome contributions! Please see our [GitHub repository](https://github.com/condecon/adaptivetesting) for:
|
|
328
335
|
|
|
329
336
|
- Issue tracking
|
|
330
|
-
- Feature requests
|
|
337
|
+
- Feature requests
|
|
331
338
|
- Pull request guidelines
|
|
332
339
|
- Development setup
|
|
333
340
|
|
|
@@ -343,6 +350,22 @@ This package is designed for researchers and practitioners in:
|
|
|
343
350
|
|
|
344
351
|
The package facilitates the transition from research simulation to real-world testing applications without requiring major code modifications.
|
|
345
352
|
|
|
353
|
+
## Citation
|
|
354
|
+
If you use this package for your academic work, please provide the following reference:
|
|
355
|
+
Engicht, J., Bee, R. M., & Koch, T. (2025). Customizable Bayesian Adaptive Testing with Python – The adaptivetesting Package. Open Science Framework. https://doi.org/10.31219/osf.io/d2xge_v1
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
@online{engichtCustomizableBayesianAdaptive2025,
|
|
359
|
+
title = {Customizable {{Bayesian Adaptive Testing}} with {{Python}} – {{The}} Adaptivetesting {{Package}}},
|
|
360
|
+
author = {Engicht, Jonas and Bee, R. Maximilian and Koch, Tobias},
|
|
361
|
+
date = {2025-08-06},
|
|
362
|
+
eprinttype = {Open Science Framework},
|
|
363
|
+
doi = {10.31219/osf.io/d2xge_v1},
|
|
364
|
+
url = {https://osf.io/d2xge_v1},
|
|
365
|
+
pubstate = {prepublished}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
346
369
|
## License
|
|
347
370
|
|
|
348
371
|
This project is licensed under the terms specified in the [LICENSE](LICENSE) file.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- **Bayesian Methods**: Built-in support for Bayesian ability estimation with customizable priors
|
|
12
12
|
- **Flexible Architecture**: Object-oriented design with abstract classes for easy extension
|
|
13
13
|
- **Item Response Theory**: Full support for 1PL, 2PL, 3PL, and 4PL models
|
|
14
|
-
- **Multiple Estimators**:
|
|
14
|
+
- **Multiple Estimators**:
|
|
15
15
|
- Maximum Likelihood Estimation (MLE)
|
|
16
16
|
- Bayesian Modal Estimation (BM)
|
|
17
17
|
- Expected A Posteriori (EAP)
|
|
@@ -113,25 +113,31 @@ adaptive_test = TestAssembler(
|
|
|
113
113
|
### Real-world Testing (Non-simulation) with PsychoPy
|
|
114
114
|
|
|
115
115
|
```python
|
|
116
|
+
# setup item pool
|
|
117
|
+
# the item pool is retrieved from the PREVIC
|
|
118
|
+
# https://github.com/manuelbohn/previc/tree/main/saves
|
|
119
|
+
import pandas as pd
|
|
120
|
+
from adaptivetesting.models import ItemPool
|
|
116
121
|
from psychopy import visual, event
|
|
117
122
|
from psychopy.hardware import keyboard
|
|
118
123
|
from adaptivetesting.implementations import TestAssembler
|
|
119
124
|
from adaptivetesting.models import AdaptiveTest, ItemPool, TestItem
|
|
120
125
|
from adaptivetesting.data import CSVContext
|
|
121
|
-
from adaptivetesting.math.estimators import
|
|
126
|
+
from adaptivetesting.math.estimators import ExpectedAPosteriori, CustomPrior
|
|
122
127
|
from adaptivetesting.math.item_selection import maximum_information_criterion
|
|
123
128
|
from scipy.stats import t
|
|
124
129
|
import pandas as pd
|
|
125
130
|
|
|
131
|
+
previc_item_pool = pd.read_csv("item_pool.csv")
|
|
132
|
+
# add item column
|
|
133
|
+
previc_item_pool["id"] = list(range(1, 90))
|
|
134
|
+
previc_item_pool.head()
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
|
|
132
|
-
"d": [0.87, 0.93, 1.0, 0.89, 0.94] # upper asymptote
|
|
133
|
-
})
|
|
134
|
-
item_pool = ItemPool.load_from_dataframe(items_data)
|
|
136
|
+
|
|
137
|
+
item_pool = ItemPool.load_from_list(
|
|
138
|
+
b=previc_item_pool["Difficulty"],
|
|
139
|
+
ids=previc_item_pool["id"]
|
|
140
|
+
)
|
|
135
141
|
|
|
136
142
|
# Create adaptive test
|
|
137
143
|
adaptive_test: AdaptiveTest = TestAssembler(
|
|
@@ -161,12 +167,12 @@ win = visual.Window([800, 600],
|
|
|
161
167
|
# init keyboard
|
|
162
168
|
keyboard.Keyboard()
|
|
163
169
|
|
|
170
|
+
## FIX THIS
|
|
164
171
|
|
|
165
172
|
# define function to get user input
|
|
166
173
|
def get_response(item: TestItem) -> int:
|
|
167
|
-
#
|
|
168
|
-
|
|
169
|
-
stimuli: str = [item for item in item_pool.test_items if item["Difficulty"] == item_difficulty][0]["word"]
|
|
174
|
+
# select corresponding word from item pool data frame
|
|
175
|
+
stimuli: str = previc_item_pool[previc_item_pool["id"] == item.id]["word"].values[0]
|
|
170
176
|
|
|
171
177
|
# create text box and display stimulus
|
|
172
178
|
text_box = visual.TextBox2(win=win,
|
|
@@ -291,17 +297,18 @@ adaptive_test = TestAssembler(
|
|
|
291
297
|
Full documentation is available in the `docs/` directory:
|
|
292
298
|
|
|
293
299
|
- [API Reference](docs/readme.md)
|
|
294
|
-
- [Models Module](docs/
|
|
295
|
-
- [Math Module](docs/
|
|
296
|
-
- [Implementation Examples](docs/
|
|
297
|
-
- [Simulation Guide](docs/
|
|
300
|
+
- [Models Module](docs/models.md)
|
|
301
|
+
- [Math Module](docs/math.md)
|
|
302
|
+
- [Implementation Examples](docs/implementations.md)
|
|
303
|
+
- [Simulation Guide](docs/simulation.md)
|
|
298
304
|
|
|
299
305
|
## Testing
|
|
300
306
|
|
|
301
307
|
The package includes comprehensive tests. Run them using:
|
|
302
308
|
|
|
303
309
|
```bash
|
|
304
|
-
|
|
310
|
+
uv sync
|
|
311
|
+
uv run python -m unittest
|
|
305
312
|
```
|
|
306
313
|
|
|
307
314
|
## Contributing
|
|
@@ -309,7 +316,7 @@ python -m pytest adaptivetesting/tests/
|
|
|
309
316
|
We welcome contributions! Please see our [GitHub repository](https://github.com/condecon/adaptivetesting) for:
|
|
310
317
|
|
|
311
318
|
- Issue tracking
|
|
312
|
-
- Feature requests
|
|
319
|
+
- Feature requests
|
|
313
320
|
- Pull request guidelines
|
|
314
321
|
- Development setup
|
|
315
322
|
|
|
@@ -325,6 +332,22 @@ This package is designed for researchers and practitioners in:
|
|
|
325
332
|
|
|
326
333
|
The package facilitates the transition from research simulation to real-world testing applications without requiring major code modifications.
|
|
327
334
|
|
|
335
|
+
## Citation
|
|
336
|
+
If you use this package for your academic work, please provide the following reference:
|
|
337
|
+
Engicht, J., Bee, R. M., & Koch, T. (2025). Customizable Bayesian Adaptive Testing with Python – The adaptivetesting Package. Open Science Framework. https://doi.org/10.31219/osf.io/d2xge_v1
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
@online{engichtCustomizableBayesianAdaptive2025,
|
|
341
|
+
title = {Customizable {{Bayesian Adaptive Testing}} with {{Python}} – {{The}} Adaptivetesting {{Package}}},
|
|
342
|
+
author = {Engicht, Jonas and Bee, R. Maximilian and Koch, Tobias},
|
|
343
|
+
date = {2025-08-06},
|
|
344
|
+
eprinttype = {Open Science Framework},
|
|
345
|
+
doi = {10.31219/osf.io/d2xge_v1},
|
|
346
|
+
url = {https://osf.io/d2xge_v1},
|
|
347
|
+
pubstate = {prepublished}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
328
351
|
## License
|
|
329
352
|
|
|
330
353
|
This project is licensed under the terms specified in the [LICENSE](LICENSE) file.
|
|
@@ -12,22 +12,50 @@ def probability_y1(mu: np.ndarray,
|
|
|
12
12
|
|
|
13
13
|
Args:
|
|
14
14
|
mu (np.ndarray): latent ability level
|
|
15
|
-
|
|
16
15
|
a (np.ndarray): item discrimination parameter
|
|
17
|
-
|
|
18
16
|
b (np.ndarray): item difficulty parameter
|
|
19
|
-
|
|
20
17
|
c (np.ndarray): pseudo guessing parameter
|
|
21
|
-
|
|
22
18
|
d (np.ndarray): inattention parameter
|
|
23
19
|
|
|
24
20
|
Returns:
|
|
25
21
|
np.ndarray: probability of getting the item correct
|
|
26
22
|
"""
|
|
23
|
+
# Compute the exponent safely
|
|
24
|
+
z = a * (mu - b)
|
|
25
|
+
|
|
26
|
+
# Use log-sum-exp trick for numerical stability
|
|
27
|
+
# For large positive z: exp(z)/(1+exp(z)) ≈ 1
|
|
28
|
+
# For large negative z: exp(z)/(1+exp(z)) ≈ exp(z)
|
|
29
|
+
|
|
30
|
+
# Clip z to prevent overflow in exp()
|
|
31
|
+
z_clipped = np.clip(z, -500, 500) # exp(-500) and exp(500) are safe
|
|
32
|
+
|
|
33
|
+
# Compute the logistic function safely
|
|
34
|
+
# Use different formulas based on the sign of z for stability
|
|
35
|
+
result = np.empty_like(z_clipped)
|
|
36
|
+
|
|
37
|
+
# For large positive values, use alternative formula to avoid overflow
|
|
38
|
+
mask_positive = z_clipped > 20 # exp(20) is large but manageable
|
|
39
|
+
mask_negative = z_clipped < -20 # exp(-20) is very small
|
|
40
|
+
mask_medium = ~(mask_positive | mask_negative)
|
|
41
|
+
|
|
42
|
+
# Large positive z: 1/(1+exp(-z)) is more stable
|
|
43
|
+
result[mask_positive] = 1.0 / (1.0 + np.exp(-z_clipped[mask_positive]))
|
|
44
|
+
|
|
45
|
+
# Large negative z: exp(z)/(1+exp(z)) is stable
|
|
46
|
+
exp_z_neg = np.exp(z_clipped[mask_negative])
|
|
47
|
+
result[mask_negative] = exp_z_neg / (1.0 + exp_z_neg)
|
|
48
|
+
|
|
49
|
+
# Medium values: use standard formula
|
|
50
|
+
exp_z_med = np.exp(z_clipped[mask_medium])
|
|
51
|
+
result[mask_medium] = exp_z_med / (1.0 + exp_z_med)
|
|
52
|
+
|
|
53
|
+
# Apply the 4PL transformation
|
|
54
|
+
value = c + (d - c) * result
|
|
55
|
+
|
|
56
|
+
# Ensure probabilities are within valid range [c, d]
|
|
57
|
+
value = np.clip(value, c, d)
|
|
27
58
|
|
|
28
|
-
value = c + (d - c) * (np.exp(a * (mu - b))) / \
|
|
29
|
-
(1 + np.exp(a * (mu - b)))
|
|
30
|
-
|
|
31
59
|
return np.squeeze(value)
|
|
32
60
|
|
|
33
61
|
|
|
@@ -40,13 +68,13 @@ def probability_y0(mu: np.ndarray,
|
|
|
40
68
|
|
|
41
69
|
Args:
|
|
42
70
|
mu (np.ndarray): latent ability level
|
|
43
|
-
|
|
71
|
+
|
|
44
72
|
a (np.ndarray): item discrimination parameter
|
|
45
|
-
|
|
73
|
+
|
|
46
74
|
b (np.ndarray): item difficulty parameter
|
|
47
|
-
|
|
75
|
+
|
|
48
76
|
c (np.ndarray): pseudo guessing parameter
|
|
49
|
-
|
|
77
|
+
|
|
50
78
|
d (np.ndarray): inattention parameter
|
|
51
79
|
|
|
52
80
|
Returns:
|
|
@@ -68,13 +96,13 @@ def likelihood(mu: np.ndarray,
|
|
|
68
96
|
|
|
69
97
|
Args:
|
|
70
98
|
mu (np.ndarray): ability level
|
|
71
|
-
|
|
99
|
+
|
|
72
100
|
a (np.ndarray): item discrimination parameter
|
|
73
|
-
|
|
101
|
+
|
|
74
102
|
b (np.ndarray): item difficulty parameter
|
|
75
|
-
|
|
103
|
+
|
|
76
104
|
c (np.ndarray): pseudo guessing parameter
|
|
77
|
-
|
|
105
|
+
|
|
78
106
|
d (np.ndarray): inattention parameter
|
|
79
107
|
|
|
80
108
|
Returns:
|
|
@@ -88,7 +116,7 @@ def likelihood(mu: np.ndarray,
|
|
|
88
116
|
|
|
89
117
|
terms = (probability_y1(mu, a, b, c, d)**response_pattern) * \
|
|
90
118
|
(probability_y0(mu, a, b, c, d) ** (1 - response_pattern))
|
|
91
|
-
|
|
119
|
+
|
|
92
120
|
return -np.prod(terms)
|
|
93
121
|
|
|
94
122
|
|
|
@@ -100,14 +128,14 @@ def maximize_likelihood_function(a: np.ndarray,
|
|
|
100
128
|
border: tuple[float, float] = (-10, 10)) -> float:
|
|
101
129
|
"""Find the ability value that maximizes the likelihood function.
|
|
102
130
|
This function uses the minimize_scalar function from scipy and the "bounded" method.
|
|
103
|
-
|
|
131
|
+
|
|
104
132
|
Args:
|
|
105
133
|
a (np.ndarray): item discrimination parameter
|
|
106
|
-
|
|
134
|
+
|
|
107
135
|
b (np.ndarray): item difficulty parameter
|
|
108
|
-
|
|
136
|
+
|
|
109
137
|
c (np.ndarray): pseudo guessing parameter
|
|
110
|
-
|
|
138
|
+
|
|
111
139
|
d (np.ndarray): inattention parameter
|
|
112
140
|
|
|
113
141
|
response_pattern (np.ndarray): response pattern of the item
|
|
@@ -129,7 +157,7 @@ def maximize_likelihood_function(a: np.ndarray,
|
|
|
129
157
|
"Response pattern is invalid. It consists of only one type of response.")
|
|
130
158
|
raise AlgorithmException(
|
|
131
159
|
"Response pattern is invalid. It consists of only one type of response.")
|
|
132
|
-
|
|
160
|
+
|
|
133
161
|
result: OptimizeResult = minimize_scalar(likelihood, args=(a, b, c, d, response_pattern),
|
|
134
162
|
bounds=border, method='bounded') # type: ignore
|
|
135
163
|
|
|
@@ -13,27 +13,17 @@ def item_information_function(
|
|
|
13
13
|
c: np.ndarray,
|
|
14
14
|
d: np.ndarray
|
|
15
15
|
) -> np.ndarray:
|
|
16
|
-
"""Calculate the information of a test item given the currently
|
|
17
|
-
estimated ability `mu`.
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
mu (np.ndarray): currently estimated ability
|
|
21
|
-
a (np.ndarray): _description_
|
|
22
|
-
b (np.ndarray): _description_
|
|
23
|
-
c (np.ndarray): _description_
|
|
24
|
-
d (np.ndarray): _description_
|
|
25
|
-
|
|
26
|
-
Returns:
|
|
27
|
-
np.ndarray: _description_
|
|
28
|
-
"""
|
|
29
16
|
p_y1 = probability_y1(mu, a, b, c, d)
|
|
30
17
|
|
|
18
|
+
# Clip probabilities
|
|
19
|
+
p_y1_clipped = np.clip(p_y1, 1e-10, 1 - 1e-10)
|
|
20
|
+
|
|
31
21
|
def p_y1_grad(x: np.ndarray) -> np.ndarray:
|
|
32
|
-
#
|
|
33
|
-
h = 1e-
|
|
22
|
+
# Use a more robust finite difference scheme
|
|
23
|
+
h = np.maximum(1e-8, 1e-8 * np.abs(x)) # Adaptive step size
|
|
34
24
|
return (probability_y1(x + h, a, b, c, d) - probability_y1(x - h, a, b, c, d)) / (2 * h)
|
|
35
|
-
|
|
36
|
-
product = (p_y1_grad(mu) ** 2) / (
|
|
25
|
+
|
|
26
|
+
product = (p_y1_grad(mu) ** 2) / (p_y1_clipped * (1 - p_y1_clipped))
|
|
37
27
|
information = np.sum(product)
|
|
38
28
|
return information
|
|
39
29
|
|
|
@@ -59,7 +49,7 @@ def prior_information_function(prior: Prior,
|
|
|
59
49
|
(score_values ** 2) * prior.pdf(x_vals),
|
|
60
50
|
x_vals
|
|
61
51
|
)
|
|
62
|
-
|
|
52
|
+
|
|
63
53
|
return information
|
|
64
54
|
|
|
65
55
|
|
|
@@ -37,7 +37,7 @@ class ItemPool:
|
|
|
37
37
|
return selected_item, simulated_response
|
|
38
38
|
else:
|
|
39
39
|
return selected_item
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
def get_item_by_item(self, item: TestItem) -> Tuple[TestItem, int] | TestItem:
|
|
42
42
|
"""Returns item and if defined the simulated response.
|
|
43
43
|
|
|
@@ -94,24 +94,27 @@ class ItemPool:
|
|
|
94
94
|
a: List[float] | None = None,
|
|
95
95
|
c: List[float] | None = None,
|
|
96
96
|
d: List[float] | None = None,
|
|
97
|
-
simulated_responses: List[int] | None = None
|
|
97
|
+
simulated_responses: List[int] | None = None,
|
|
98
|
+
ids: List[int] | None = None) -> "ItemPool":
|
|
98
99
|
"""
|
|
99
100
|
Creates test items from a list of floats.
|
|
100
101
|
|
|
101
102
|
Args:
|
|
102
103
|
a (List[float]): discrimination parameter
|
|
103
|
-
|
|
104
|
+
|
|
104
105
|
b (List[float]): difficulty parameter
|
|
105
|
-
|
|
106
|
+
|
|
106
107
|
c (List[float]): guessing parameter
|
|
107
|
-
|
|
108
|
+
|
|
108
109
|
d (List[float]): slipping parameter
|
|
109
|
-
|
|
110
|
+
|
|
110
111
|
simulated_responses (List[int]): simulated responses
|
|
111
112
|
|
|
113
|
+
ids (List[int]): item IDs
|
|
114
|
+
|
|
112
115
|
Returns:
|
|
113
116
|
List[TestItem]: item pool
|
|
114
|
-
|
|
117
|
+
|
|
115
118
|
"""
|
|
116
119
|
items: List[TestItem] = []
|
|
117
120
|
|
|
@@ -126,19 +129,25 @@ class ItemPool:
|
|
|
126
129
|
raise ValueError("Length of a and b has to be the same.")
|
|
127
130
|
for i, discrimination in enumerate(a):
|
|
128
131
|
items[i].a = discrimination
|
|
129
|
-
|
|
132
|
+
|
|
130
133
|
if c is not None:
|
|
131
134
|
if len(c) != len(b):
|
|
132
135
|
raise ValueError("Length of c and b has to be the same.")
|
|
133
136
|
for i, guessing in enumerate(c):
|
|
134
137
|
items[i].c = guessing
|
|
135
|
-
|
|
138
|
+
|
|
136
139
|
if d is not None:
|
|
137
140
|
if len(d) != len(b):
|
|
138
141
|
raise ValueError("Length of d and b has to be the same.")
|
|
139
142
|
for i, slipping in enumerate(d):
|
|
140
143
|
items[i].d = slipping
|
|
141
144
|
|
|
145
|
+
if ids is not None:
|
|
146
|
+
if len(ids) != len(b):
|
|
147
|
+
raise ValueError("Length of ids and b has to be the same.")
|
|
148
|
+
for i, id_ in enumerate(ids):
|
|
149
|
+
items[i].id = id_
|
|
150
|
+
|
|
142
151
|
item_pool = ItemPool(items)
|
|
143
152
|
item_pool.simulated_responses = simulated_responses
|
|
144
153
|
|
|
@@ -146,10 +155,11 @@ class ItemPool:
|
|
|
146
155
|
|
|
147
156
|
@staticmethod
|
|
148
157
|
def load_from_dict(source: dict[str, List[float]],
|
|
149
|
-
simulated_responses: List[int] | None = None
|
|
158
|
+
simulated_responses: List[int] | None = None,
|
|
159
|
+
ids: List[int] | None = None) -> "ItemPool":
|
|
150
160
|
"""Creates test items from a dictionary.
|
|
151
161
|
The dictionary has to have the following keys:
|
|
152
|
-
|
|
162
|
+
|
|
153
163
|
- a
|
|
154
164
|
- b
|
|
155
165
|
- c
|
|
@@ -158,6 +168,8 @@ class ItemPool:
|
|
|
158
168
|
|
|
159
169
|
Args:
|
|
160
170
|
source (dict[str, List[float]]): item pool dictionary
|
|
171
|
+
simulated_responses (List[int]): simulated responses
|
|
172
|
+
ids (List[int]): item IDs
|
|
161
173
|
|
|
162
174
|
Returns:
|
|
163
175
|
List[TestItem]: item pool
|
|
@@ -170,13 +182,13 @@ class ItemPool:
|
|
|
170
182
|
# check none
|
|
171
183
|
if a is None:
|
|
172
184
|
raise ValueError("a cannot be None")
|
|
173
|
-
|
|
185
|
+
|
|
174
186
|
if b is None:
|
|
175
187
|
raise ValueError("b cannot be None")
|
|
176
|
-
|
|
188
|
+
|
|
177
189
|
if c is None:
|
|
178
190
|
raise ValueError("c cannot be None")
|
|
179
|
-
|
|
191
|
+
|
|
180
192
|
if d is None:
|
|
181
193
|
raise ValueError("d cannot be None")
|
|
182
194
|
|
|
@@ -184,6 +196,10 @@ class ItemPool:
|
|
|
184
196
|
if not (len(a) == len(b) == len(c) == len(d)):
|
|
185
197
|
raise ValueError("All lists in the source dictionary must have the same length")
|
|
186
198
|
|
|
199
|
+
if ids is not None:
|
|
200
|
+
if len(ids) != len(b):
|
|
201
|
+
raise ValueError("Length of ids and b has to be the same.")
|
|
202
|
+
|
|
187
203
|
n_items = len(b)
|
|
188
204
|
items: List[TestItem] = []
|
|
189
205
|
for i in range(n_items):
|
|
@@ -193,12 +209,15 @@ class ItemPool:
|
|
|
193
209
|
item.c = c[i]
|
|
194
210
|
item.d = d[i]
|
|
195
211
|
|
|
212
|
+
if ids is not None:
|
|
213
|
+
item.id = ids[i]
|
|
214
|
+
|
|
196
215
|
items.append(item)
|
|
197
216
|
|
|
198
217
|
item_pool = ItemPool(items, simulated_responses)
|
|
199
|
-
|
|
218
|
+
|
|
200
219
|
return item_pool
|
|
201
|
-
|
|
220
|
+
|
|
202
221
|
@staticmethod
|
|
203
222
|
def load_from_dataframe(source: DataFrame) -> "ItemPool":
|
|
204
223
|
"""Creates item pool from a pandas DataFrame.
|
|
@@ -206,7 +225,7 @@ class ItemPool:
|
|
|
206
225
|
Each column has to contain float values.
|
|
207
226
|
A `simulated_responses` (int values) column can be added to
|
|
208
227
|
the DataFrame to provide simulated responses.
|
|
209
|
-
|
|
228
|
+
|
|
210
229
|
|
|
211
230
|
Args:
|
|
212
231
|
source (DataFrame): _description_
|
|
@@ -218,28 +237,33 @@ class ItemPool:
|
|
|
218
237
|
# check if columns are present
|
|
219
238
|
if "a" not in source.columns:
|
|
220
239
|
raise ValueError("Column 'a' not found.")
|
|
221
|
-
|
|
240
|
+
|
|
222
241
|
if "b" not in source.columns:
|
|
223
242
|
raise ValueError("Column 'b' not found.")
|
|
224
|
-
|
|
243
|
+
|
|
225
244
|
if "c" not in source.columns:
|
|
226
245
|
raise ValueError("Column 'c' not found.")
|
|
227
|
-
|
|
246
|
+
|
|
228
247
|
if "d" not in source.columns:
|
|
229
248
|
raise ValueError("Column 'd' not found.")
|
|
230
|
-
|
|
249
|
+
|
|
231
250
|
# get values
|
|
232
251
|
a: List[float] = source["a"].values.tolist() # type: ignore
|
|
233
252
|
b: List[float] = source["b"].values.tolist() # type: ignore
|
|
234
253
|
c: List[float] = source["c"].values.tolist() # type: ignore
|
|
235
254
|
d: List[float] = source["d"].values.tolist() # type: ignore
|
|
236
255
|
|
|
256
|
+
if "ids" in source.columns:
|
|
257
|
+
ids: List[int] | None = source["ids"].values.tolist() # type: ignore
|
|
258
|
+
else:
|
|
259
|
+
ids = None
|
|
260
|
+
|
|
237
261
|
# create item pool
|
|
238
|
-
item_pool = ItemPool.load_from_list(a=a, b=b, c=c, d=d)
|
|
239
|
-
|
|
262
|
+
item_pool = ItemPool.load_from_list(a=a, b=b, c=c, d=d, ids=ids)
|
|
263
|
+
|
|
240
264
|
# check if simulated responses are present
|
|
241
265
|
if "simulated_responses" in source.columns:
|
|
242
266
|
simulated_responses: List[int] = source["simulated_responses"].values.tolist() # type: ignore
|
|
243
267
|
item_pool.simulated_responses = simulated_responses
|
|
244
|
-
|
|
268
|
+
|
|
245
269
|
return item_pool
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
class TestItem:
|
|
2
|
+
def __init__(self):
|
|
3
|
+
"""Representation of a test item in the item pool.
|
|
4
|
+
The format is equal to the implementation in catR.
|
|
5
|
+
|
|
6
|
+
Properties:
|
|
7
|
+
- id (int | None): item ID
|
|
8
|
+
- a (float): discrimination parameter
|
|
9
|
+
- b (float): difficulty parameter
|
|
10
|
+
- c (float): guessing parameter
|
|
11
|
+
- d (float): slipping parameter / upper asymptote
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
self.id: int | None = None
|
|
15
|
+
self.a: float = 1
|
|
16
|
+
self.b: float = float("nan")
|
|
17
|
+
self.c: float = 0
|
|
18
|
+
self.d: float = 1
|
|
19
|
+
|
|
20
|
+
def as_dict(self, with_id: bool = False) -> dict[str, float | int | None]:
|
|
21
|
+
"""Returns the item as a dictionary"""
|
|
22
|
+
|
|
23
|
+
item_dict: dict[str, float | int | None] = {
|
|
24
|
+
"a": self.a,
|
|
25
|
+
"b": self.b,
|
|
26
|
+
"c": self.c,
|
|
27
|
+
"d": self.d
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if with_id and self.id is not None:
|
|
31
|
+
item_dict["id"] = self.id
|
|
32
|
+
|
|
33
|
+
return item_dict
|