This notebook demonstrates a compact workflow for using IXPLORE.
- Load example reaction data (binary or Likert-scale).
- Initialize and fit
IXPLORE(with and without PCA initialization). - Visualize the learned 2D embedding and decision boundaries for some questions.
- Embed a new user from partial answers and impute missing responses interpretably.
- Evaluate fit (MAE, accuracy) and examine model parameters.
import numpy as np
import pandas as pd
from ixplore import IXPLORE
from ixplore.visualization import plot_overview, plot_posterior
from ixplore.utils import transformation_matrix
Load data¶
This cell loads user metadata (for example colours or labels) used to annotate plots. The synthetic data was generated in notebooks/data.ipynb.
users = pd.read_csv('../data/synthetic_users.csv', index_col=0)
users
| x | y | party | noise | color | |
|---|---|---|---|---|---|
| id | |||||
| 0 | -0.468 | -0.583 | 0 | 0 | C0 |
| 1 | -0.244 | -0.432 | 0 | 0 | C0 |
| 2 | -0.346 | -0.412 | 0 | 0 | C0 |
| 3 | -0.514 | -0.558 | 0 | 1 | C0 |
| 4 | -0.419 | -0.586 | 0 | 4 | C0 |
| ... | ... | ... | ... | ... | ... |
| 245 | -0.154 | 0.210 | 4 | 1 | C4 |
| 246 | 0.012 | -0.171 | 4 | 1 | C4 |
| 247 | -0.079 | -0.067 | 4 | 1 | C4 |
| 248 | -0.203 | 0.279 | 4 | 5 | C4 |
| 249 | 0.270 | 0.024 | 4 | 1 | C4 |
250 rows × 5 columns
This cell loads the user-item reaction matrix. For Likert data, IXPLORE automatically normalizes responses to the [0,1] range.
reactions = pd.read_csv('../data/likert_reactions.csv', index_col=0)
reactions
| Q1 | Q2 | Q3 | Q4 | Q5 | Q6 | Q7 | Q8 | Q9 | Q10 | ... | Q61 | Q62 | Q63 | Q64 | Q65 | Q66 | Q67 | Q68 | Q69 | Q70 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| id | |||||||||||||||||||||
| 0 | 1 | 2 | 3 | 0 | 3 | 1 | 2 | 3 | 2 | 3 | ... | 0 | 2 | 2 | 1 | 2 | 2 | 3 | 1 | 2 | 2 |
| 1 | 1 | 2 | 3 | 0 | 3 | 1 | 2 | 3 | 2 | 3 | ... | 0 | 2 | 2 | 1 | 2 | 2 | 3 | 1 | 2 | 2 |
| 2 | 1 | 2 | 3 | 0 | 3 | 1 | 1 | 2 | 2 | 3 | ... | 0 | 2 | 2 | 1 | 2 | 2 | 3 | 1 | 2 | 2 |
| 3 | 1 | 2 | 3 | 0 | 3 | 1 | 2 | 3 | 2 | 3 | ... | 0 | 2 | 2 | 1 | 2 | 2 | 3 | 0 | 2 | 2 |
| 4 | 1 | 2 | 3 | 0 | 3 | 1 | 2 | 3 | 2 | 3 | ... | 0 | 2 | 2 | 1 | 2 | 2 | 3 | 1 | 2 | 2 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 245 | 2 | 2 | 3 | 1 | 3 | 2 | 0 | 0 | 2 | 1 | ... | 3 | 2 | 0 | 2 | 2 | 2 | 2 | 2 | 1 | 0 |
| 246 | 1 | 3 | 2 | 0 | 3 | 1 | 1 | 1 | 1 | 2 | ... | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 2 | 0 |
| 247 | 2 | 2 | 2 | 0 | 3 | 1 | 1 | 1 | 2 | 2 | ... | 2 | 2 | 1 | 2 | 2 | 2 | 2 | 2 | 1 | 0 |
| 248 | 2 | 1 | 3 | 1 | 3 | 2 | 0 | 0 | 2 | 1 | ... | 3 | 2 | 0 | 3 | 2 | 2 | 2 | 2 | 1 | 0 |
| 249 | 2 | 3 | 1 | 1 | 3 | 1 | 1 | 0 | 1 | 1 | ... | 3 | 1 | 2 | 2 | 1 | 1 | 2 | 3 | 2 | 0 |
250 rows × 70 columns
xplore = IXPLORE(reactions, pca_initialization=False, random_state=17, sampling_resolution=100)
ixplore - INFO - 2026-05-08 09:09:49 - IXPLORE initialized: 250 users × 70 items (0 missing, 0.00%), grid 100×100, prior var=1.0. ixplore - INFO - 2026-05-08 09:09:49 - Initial — mae: 0.2813, accuracy: 0.6741, spread: 0.6403, boundary: 0.1960
We initialize the user positions randomly (left panel) and learn logistic regression models based on these positions (right panel).
fig, (ax1, ax2) = plot_overview(xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:49 - MAE: 0.2813, ACC: 0.6741, Border: 0.1960, Spread: 0.6403
We fit posterior distributions for all users using the current logistic regression models, then visualize the posterior for one example user.
xplore.fit_posteriors()
fig, (ax1, ax2) = plot_overview(xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:49 - MAE: 0.2699, ACC: 0.7008, Border: 0.0000, Spread: 0.3199
We fit logistic regression models for each question using the current embedding as features. The decision boundaries start to be meaningful (right panel). The new posterior distribution of the inspected user got slightly modified (left panel).
xplore.fit_models()
fig, (ax1, ax2) = plot_overview(xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:49 - MAE: 0.0722, ACC: 0.9666, Border: 0.0000, Spread: 0.3199
We repeat these two steps for another four iterations and see how the mean absolute error goes down.
xplore.iterate(n_iterations=4)
fig, (ax1, ax2) = plot_overview(xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:50 - Fit complete after 4 iterations — mae: 0.0616, accuracy: 0.9854, spread: 0.3635, boundary: 0.0120 ixplore - INFO - 2026-05-08 09:09:50 - MAE: 0.0616, ACC: 0.9854, Border: 0.0120, Spread: 0.3635
We apply a linear transformation (rotation/shear/scale) to the embedding to align with typical dimensions in political landscapes.
M = transformation_matrix(rotate=55, scale=(1.1, 1), order=('shear', 'rotate', 'scale'))
xplore.transform_embedding(M)
fig, (ax1, ax2) = plot_overview(xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:50 - MAE: 0.0616, ACC: 0.9854, Border: 0.0640, Spread: 0.5940
Let's update the posteriors one more time.
xplore.fit_posteriors()
fig, (ax1, ax2) = plot_overview(xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:51 - MAE: 0.0615, ACC: 0.9850, Border: 0.0040, Spread: 0.5757
Initialize with transformation¶
We can then later initialize the model directly with this predefined transformation matrix.
M = transformation_matrix(rotate=55, scale=(1.1, 1), order=('shear', 'rotate', 'scale'))
transformed_xplore = IXPLORE(reactions, pca_initialization=False, random_state=17, transformation=M)
transformed_xplore.iterate(n_iterations=5)
fig, (ax1, ax2) = plot_overview(transformed_xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:51 - IXPLORE initialized: 250 users × 70 items (0 missing, 0.00%), grid 200×200, prior var=1.0. ixplore - INFO - 2026-05-08 09:09:51 - Initial — mae: 0.2813, accuracy: 0.6741, spread: 0.7084, boundary: 0.2880 ixplore - INFO - 2026-05-08 09:09:52 - Fit complete after 5 iterations — mae: 0.0614, accuracy: 0.9853, spread: 0.4198, boundary: 0.0000 ixplore - INFO - 2026-05-08 09:09:52 - MAE: 0.0614, ACC: 0.9853, Border: 0.0000, Spread: 0.4198
We can save the embedding coordinates and model parameters to load them directly later.
path = '../data/pretrained_embedding_likert.csv'
transformed_xplore.get_embedding().to_csv(path)
path = '../data/pretrained_models_likert.csv'
transformed_xplore.get_parameters().to_csv(path)
Initialize with PCA¶
We can directly initialize the embedding with PCA to speed up the convergence.
pca_xplore = IXPLORE(reactions, pca_initialization=True, random_state=17)
fig, (ax1, ax2) = plot_overview(pca_xplore, question='Q15', user='159', colors=users.color)
ixplore - INFO - 2026-05-08 09:09:52 - IXPLORE initialized: 250 users × 70 items (0 missing, 0.00%), grid 200×200, prior var=1.0. ixplore - INFO - 2026-05-08 09:09:52 - Initial — mae: 0.0645, accuracy: 0.9810, spread: 0.6116, boundary: 0.0560 ixplore - INFO - 2026-05-08 09:09:52 - MAE: 0.0645, ACC: 0.9810, Border: 0.0560, Spread: 0.6116
Load model¶
We can load a model with pretrained embedding and/or item parameters.
embedding = pd.read_csv('../data/pretrained_embedding_likert.csv', index_col=0)
models = pd.read_csv('../data/pretrained_models_likert.csv', index_col=0)
loaded_xplore = IXPLORE(reactions,
pretrained_embedding=embedding,
pretrained_models=models)
fig, (ax1, ax2) = plot_overview(loaded_xplore, question='Q15', user=None, colors=users.color)
ixplore - INFO - 2026-05-08 09:09:53 - IXPLORE initialized: 250 users × 70 items (0 missing, 0.00%), grid 200×200, prior var=1.0. ixplore - INFO - 2026-05-08 09:09:53 - Initial — mae: 0.0614, accuracy: 0.9853, spread: 0.4198, boundary: 0.0000 ixplore - INFO - 2026-05-08 09:09:53 - MAE: 0.0614, ACC: 0.9853, Border: 0.0000, Spread: 0.4198
We can also embed a new user that has only answered one question and see where they would be located in the embedding space.
new_user = pd.Series({'Q15':0}, name='new_user')
fig, (ax1, ax2) = plot_overview(loaded_xplore, question='Q15', user=None, colors=users.color)
fig, ax1 = plot_posterior(loaded_xplore, new_user, ax=ax1)
ixplore - INFO - 2026-05-08 09:09:53 - MAE: 0.0614, ACC: 0.9853, Border: 0.0000, Spread: 0.4198
Let's say, we want to create a user that is at the very left of the embedding space. We can then predict answers for this position.
answers = loaded_xplore.predict(np.array([[-0.55,0.1]])).flatten().round()
new_user = pd.Series(answers, index=loaded_xplore.items, name='new_user')
new_user.head()
Q1 1.0 Q2 0.0 Q3 1.0 Q4 0.0 Q5 1.0 Name: new_user, dtype: float64
If we embed this user, we see that they end up in the left area.
fig, (ax1, ax2) = plot_overview(loaded_xplore, question='Q1', user=None, colors=users.color)
x, y = loaded_xplore.embed(new_user)
ax1.scatter(x, y, color='white', edgecolors='black', s=60, zorder=5)
fig, ax1 = plot_posterior(loaded_xplore, new_user, ax=ax1)
ax1.set_title("Posterior distribution for a new user")
ixplore - INFO - 2026-05-08 09:11:53 - MAE: 0.0614, ACC: 0.9853, Border: 0.0000, Spread: 0.4198