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.
In [1]:
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.

In [2]:
users = pd.read_csv('../data/synthetic_users.csv', index_col=0)
users
Out[2]:
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.

In [3]:
reactions = pd.read_csv('../data/likert_reactions.csv', index_col=0)
reactions
Out[3]:
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

Train models¶

Random initialization¶

In [4]:
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).

In [6]:
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
No description has been provided for this image

We fit posterior distributions for all users using the current logistic regression models, then visualize the posterior for one example user.

In [7]:
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
No description has been provided for this image

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).

In [8]:
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
No description has been provided for this image

We repeat these two steps for another four iterations and see how the mean absolute error goes down.

In [9]:
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
No description has been provided for this image

We apply a linear transformation (rotation/shear/scale) to the embedding to align with typical dimensions in political landscapes.

In [10]:
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
No description has been provided for this image

Let's update the posteriors one more time.

In [11]:
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
No description has been provided for this image

Initialize with transformation¶

We can then later initialize the model directly with this predefined transformation matrix.

In [12]:
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
No description has been provided for this image

We can save the embedding coordinates and model parameters to load them directly later.

In [13]:
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.

In [14]:
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
No description has been provided for this image

Load model¶

We can load a model with pretrained embedding and/or item parameters.

In [15]:
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
No description has been provided for this image

We can also embed a new user that has only answered one question and see where they would be located in the embedding space.

In [16]:
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
No description has been provided for this image

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.

In [17]:
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()
Out[17]:
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.

In [20]:
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
No description has been provided for this image