Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Contributing Guidelines

Thank you for your interest in contributing!

- All pull requests **must** be opened against the `dev` branch.
- Ensure your code passes all test workflows:
- `mypy` (type checking)
- `flake8` (linting)
- Unit tests

Pull requests that meet these requirements will be considered for merging.
22 changes: 22 additions & 0 deletions .readthedocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the OS, Python version, and other tools you might need
build:
os: ubuntu-24.04
tools:
python: "3.13"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/source/conf.py

python:
install:
- method: pip
path: .
- requirements: docs/requirements.txt

65 changes: 0 additions & 65 deletions BREAKING_CHANGES.md

This file was deleted.

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Version 1.1.2

- The `ItemPool` class now correctly honors the `id` field of items. (#30, #31)
- Examples and the readme file have been updated accordingly.
37 changes: 22 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
- **Bayesian Methods**: Built-in support for Bayesian ability estimation with customizable priors
- **Flexible Architecture**: Object-oriented design with abstract classes for easy extension
- **Item Response Theory**: Full support for 1PL, 2PL, 3PL, and 4PL models
- **Multiple Estimators**:
- **Multiple Estimators**:
- Maximum Likelihood Estimation (MLE)
- Bayesian Modal Estimation (BM)
- Expected A Posteriori (EAP)
Expand Down Expand Up @@ -113,25 +113,31 @@ adaptive_test = TestAssembler(
### Real-world Testing (Non-simulation) with PsychoPy

```python
# setup item pool
# the item pool is retrieved from the PREVIC
# https://github.com/manuelbohn/previc/tree/main/saves
import pandas as pd
from adaptivetesting.models import ItemPool
from psychopy import visual, event
from psychopy.hardware import keyboard
from adaptivetesting.implementations import TestAssembler
from adaptivetesting.models import AdaptiveTest, ItemPool, TestItem
from adaptivetesting.data import CSVContext
from adaptivetesting.math.estimators import BayesModal, CustomPrior
from adaptivetesting.math.estimators import ExpectedAPosteriori, CustomPrior
from adaptivetesting.math.item_selection import maximum_information_criterion
from scipy.stats import t
import pandas as pd

previc_item_pool = pd.read_csv("item_pool.csv")
# add item column
previc_item_pool["id"] = list(range(1, 90))
previc_item_pool.head()

# Create item pool from DataFrame
items_data = pd.DataFrame({
"a": [1.32, 1.07, 0.84, 1.19, 0.95], # discrimination
"b": [-0.63, 0.18, -0.84, 0.41, -0.25], # difficulty
"c": [0.17, 0.10, 0.19, 0.15, 0.12], # guessing
"d": [0.87, 0.93, 1.0, 0.89, 0.94] # upper asymptote
})
item_pool = ItemPool.load_from_dataframe(items_data)

item_pool = ItemPool.load_from_list(
b=previc_item_pool["Difficulty"],
ids=previc_item_pool["id"]
)

# Create adaptive test
adaptive_test: AdaptiveTest = TestAssembler(
Expand Down Expand Up @@ -161,12 +167,12 @@ win = visual.Window([800, 600],
# init keyboard
keyboard.Keyboard()

## FIX THIS

# define function to get user input
def get_response(item: TestItem) -> int:
# get index
item_difficulty: float = item.b
stimuli: str = [item for item in item_pool.test_items if item["Difficulty"] == item_difficulty][0]["word"]
# select corresponding word from item pool data frame
stimuli: str = previc_item_pool[previc_item_pool["id"] == item.id]["word"].values[0]

# create text box and display stimulus
text_box = visual.TextBox2(win=win,
Expand Down Expand Up @@ -301,15 +307,16 @@ Full documentation is available in the `docs/` directory:
The package includes comprehensive tests. Run them using:

```bash
python -m pytest adaptivetesting/tests/
uv sync
uv run python -m unittest
```

## Contributing

We welcome contributions! Please see our [GitHub repository](https://github.com/condecon/adaptivetesting) for:

- Issue tracking
- Feature requests
- Feature requests
- Pull request guidelines
- Development setup

Expand Down
72 changes: 48 additions & 24 deletions adaptivetesting/models/__item_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def get_item_by_index(self, index: int) -> Tuple[TestItem, int] | TestItem:
return selected_item, simulated_response
else:
return selected_item

def get_item_by_item(self, item: TestItem) -> Tuple[TestItem, int] | TestItem:
"""Returns item and if defined the simulated response.

Expand Down Expand Up @@ -94,24 +94,27 @@ def load_from_list(
a: List[float] | None = None,
c: List[float] | None = None,
d: List[float] | None = None,
simulated_responses: List[int] | None = None) -> "ItemPool":
simulated_responses: List[int] | None = None,
ids: List[int] | None = None) -> "ItemPool":
"""
Creates test items from a list of floats.

Args:
a (List[float]): discrimination parameter

b (List[float]): difficulty parameter

c (List[float]): guessing parameter

d (List[float]): slipping parameter

simulated_responses (List[int]): simulated responses

ids (List[int]): item IDs

Returns:
List[TestItem]: item pool

"""
items: List[TestItem] = []

Expand All @@ -126,30 +129,37 @@ def load_from_list(
raise ValueError("Length of a and b has to be the same.")
for i, discrimination in enumerate(a):
items[i].a = discrimination

if c is not None:
if len(c) != len(b):
raise ValueError("Length of c and b has to be the same.")
for i, guessing in enumerate(c):
items[i].c = guessing

if d is not None:
if len(d) != len(b):
raise ValueError("Length of d and b has to be the same.")
for i, slipping in enumerate(d):
items[i].d = slipping

if ids is not None:
if len(ids) != len(b):
raise ValueError("Length of ids and b has to be the same.")
for i, id_ in enumerate(ids):
items[i].id = id_

item_pool = ItemPool(items)
item_pool.simulated_responses = simulated_responses

return item_pool

@staticmethod
def load_from_dict(source: dict[str, List[float]],
simulated_responses: List[int] | None = None) -> "ItemPool":
simulated_responses: List[int] | None = None,
ids: List[int] | None = None) -> "ItemPool":
"""Creates test items from a dictionary.
The dictionary has to have the following keys:

- a
- b
- c
Expand All @@ -158,6 +168,8 @@ def load_from_dict(source: dict[str, List[float]],

Args:
source (dict[str, List[float]]): item pool dictionary
simulated_responses (List[int]): simulated responses
ids (List[int]): item IDs

Returns:
List[TestItem]: item pool
Expand All @@ -170,20 +182,24 @@ def load_from_dict(source: dict[str, List[float]],
# check none
if a is None:
raise ValueError("a cannot be None")

if b is None:
raise ValueError("b cannot be None")

if c is None:
raise ValueError("c cannot be None")

if d is None:
raise ValueError("d cannot be None")

# check if a, b, c, and d have the same length
if not (len(a) == len(b) == len(c) == len(d)):
raise ValueError("All lists in the source dictionary must have the same length")

if ids is not None:
if len(ids) != len(b):
raise ValueError("Length of ids and b has to be the same.")

n_items = len(b)
items: List[TestItem] = []
for i in range(n_items):
Expand All @@ -193,20 +209,23 @@ def load_from_dict(source: dict[str, List[float]],
item.c = c[i]
item.d = d[i]

if ids is not None:
item.id = ids[i]

items.append(item)

item_pool = ItemPool(items, simulated_responses)

return item_pool

@staticmethod
def load_from_dataframe(source: DataFrame) -> "ItemPool":
"""Creates item pool from a pandas DataFrame.
Required columns are: `a`, `b`, `c`, `d`.
Each column has to contain float values.
A `simulated_responses` (int values) column can be added to
the DataFrame to provide simulated responses.


Args:
source (DataFrame): _description_
Expand All @@ -218,28 +237,33 @@ def load_from_dataframe(source: DataFrame) -> "ItemPool":
# check if columns are present
if "a" not in source.columns:
raise ValueError("Column 'a' not found.")

if "b" not in source.columns:
raise ValueError("Column 'b' not found.")

if "c" not in source.columns:
raise ValueError("Column 'c' not found.")

if "d" not in source.columns:
raise ValueError("Column 'd' not found.")

# get values
a: List[float] = source["a"].values.tolist() # type: ignore
b: List[float] = source["b"].values.tolist() # type: ignore
c: List[float] = source["c"].values.tolist() # type: ignore
d: List[float] = source["d"].values.tolist() # type: ignore

if "ids" in source.columns:
ids: List[int] | None = source["ids"].values.tolist() # type: ignore
else:
ids = None

# create item pool
item_pool = ItemPool.load_from_list(a=a, b=b, c=c, d=d)
item_pool = ItemPool.load_from_list(a=a, b=b, c=c, d=d, ids=ids)

# check if simulated responses are present
if "simulated_responses" in source.columns:
simulated_responses: List[int] = source["simulated_responses"].values.tolist() # type: ignore
item_pool.simulated_responses = simulated_responses

return item_pool
Loading