In [ ]:
from __future__ import division

import random
import math
import copy
import os
import pickle

import pandas as pd
import numpy as np

from matplotlib import pyplot as plt
import seaborn as sns
sns.set_style('darkgrid')

from lentil import datatools
from lentil import datasynth
from lentil import evaluate
from lentil import models
from lentil import est

%matplotlib inline

In [ ]:
import logging
logging.getLogger().setLevel(logging.DEBUG)

Generate a synthetic 1PL/2PL IRT model and sample an interaction history from it


In [ ]:
num_students = 2000
num_assessments = 3000
num_ixns_per_student = 1000

USING_2PL = False # False => using 1PL

In [ ]:
proficiencies = np.random.normal(0, 1, num_students)
difficulties = np.random.normal(0, 1, num_assessments)

if USING_2PL:
    discriminabilities = np.random.normal(0, 1, num_assessments)
else:
    discriminabilities = np.ones(num_assessments)

student_ids = ['S'+str(x) for x in xrange(num_students)]
assessment_ids = ['A'+str(x) for x in xrange(num_assessments)]

In [ ]:
ixns = [None] * (num_students * num_ixns_per_student)
assessment_idxes = range(num_assessments)
for student_idx, student_id in enumerate(student_ids):
    for t in xrange(num_ixns_per_student):
        module_idx = random.choice(assessment_idxes)
        pass_likelihood = 1 / (1 + math.exp(-(discriminabilities[module_idx]*proficiencies[student_idx] + difficulties[module_idx])))
        ixns[student_idx * num_ixns_per_student + t] = {
            'student_id' : student_id, 
            'module_id' : assessment_ids[module_idx], 
            'module_type' : datatools.AssessmentInteraction.MODULETYPE,
            'outcome' : np.random.random() < pass_likelihood, 
            'timestep' : t+1
        }
history = datatools.InteractionHistory(pd.DataFrame(ixns))
history.idx_of_student_id = lambda x: int(x[1:])
history.idx_of_assessment_id = lambda x: int(x[1:])

In [ ]:
mirt_model = models.MIRTModel(history, dims=1, using_assessment_factors=USING_2PL)
estimator = est.MIRTMAPEstimator(
    regularization_constant=1e-3,
    ftol=1e-5,
    debug_mode_on=True)
mirt_model.fit(estimator)

In [ ]:
onepl_model = models.OneParameterLogisticModel(
    history.data, select_regularization_constant=True)
onepl_model.fit()

In [ ]:
twopl_model = models.TwoParameterLogisticModel(
    history.data, select_regularization_constant=True)
twopl_model.fit()

In [ ]:
student_idxes = [int(k[1:]) for k in history.data['student_id'].unique()]
assessment_idxes = [int(k[1:]) for k in history.data['module_id'].unique()]

Verify that models.OneParameterLogisticModel can recover parameters. We would only expect this to be possible when USING_2PL = False.


In [ ]:
plt.xlabel('True difficulties')
plt.ylabel('Estimated difficulties')
plt.scatter(difficulties[assessment_idxes], onepl_model.model.coef_[0, num_students:])
plt.show()

plt.xlabel('Estimated difficulty - true difficulty')
plt.ylabel('Frequency (number of assessments)')
plt.hist(onepl_model.model.coef_[0, num_students:] - difficulties[assessment_idxes], bins=20)
plt.show()

In [ ]:
plt.xlabel('True proficiencies')
plt.ylabel('Estimated proficiencies')
plt.scatter(proficiencies[student_idxes], onepl_model.model.coef_[0, :num_students])
plt.show()

plt.xlabel('Estimated proficiency - true proficiency')
plt.ylabel('Frequency (number of students)')
plt.hist(onepl_model.model.coef_[0, :num_students] - proficiencies[student_idxes], bins=20)
plt.show()

Verify that models.TwoParameterLogisticModel can recover parameters. We would only expect this to be possible when USING_2PL = True.


In [ ]:
plt.xlabel('True difficulties')
plt.ylabel('Estimated difficulties')
plt.scatter(difficulties[assessment_idxes], twopl_model.model.coef_[0, (num_students*num_assessments):])
plt.show()

plt.xlabel('Estimated difficulty - true difficulty')
plt.ylabel('Frequency (number of assessments)')
plt.hist(twopl_model.model.coef_[0, (num_students*num_assessments):] - difficulties[assessment_idxes], bins=20)
plt.show()

In [ ]:
est_params = twopl_model.model.coef_[0, :(num_students*num_assessments)]
true_params = discriminabilities[:, None].dot(proficiencies[:, None].T).ravel()

plt.xlabel('True proficiency*discriminability')
plt.ylabel('Estimated proficiency*discriminability')
plt.scatter(true_params, est_params)
plt.show()

plt.xlabel('Estimated proficiency*discriminability - true proficiency*discriminability')
plt.ylabel('Frequency (number of student-assessment pairs)')
plt.hist(est_params - true_params, bins=20)
plt.show()

Verify that models.MIRTModel can recover parameters


In [ ]:
plt.xlabel('True difficulties')
plt.ylabel('Estimated difficulties')
plt.scatter(difficulties, mirt_model.assessment_offsets)
plt.show()

plt.xlabel('Estimated difficulty - true difficulty')
plt.ylabel('Frequency (number of assessments)')
plt.hist(mirt_model.assessment_offsets - difficulties, bins=20)
plt.show()

In [ ]:
plt.xlabel('True proficiencies')
plt.ylabel('Estimated proficiencies')
plt.scatter(proficiencies, mirt_model.student_factors[:, 0])
plt.show()

plt.xlabel('Estimated proficiency - true proficiency')
plt.ylabel('Frequency (number of students)')
plt.hist(mirt_model.student_factors[:, 0] - proficiencies, bins=20)
plt.show()

In [ ]:
plt.xlabel('True discriminabilities')
plt.ylabel('Estimated discriminabilities')
plt.scatter(discriminabilities, mirt_model.assessment_factors[:, 0])
plt.show()

plt.xlabel('Estimated discriminability - true discriminability')
plt.ylabel('Frequency (number of assessments)')
plt.hist(mirt_model.assessment_factors[:, 0] - discriminabilities, bins=20)
plt.show()

Verify that all models achieve similar training AUCs


In [ ]:
# models.OneParameterLogisticModel
evaluate.training_auc(onepl_model, history, plot_roc_curve=True)

In [ ]:
# models.TwoParameterLogisticModel
evaluate.training_auc(twopl_model, history, plot_roc_curve=True)

In [ ]:
# models.MIRTModel
evaluate.training_auc(mirt_model, history, plot_roc_curve=True)

In [ ]:
# true model
true_model = copy.deepcopy(mirt_model)
true_model.student_factors[:, 0] = proficiencies
true_model.assessment_factors[:, 0] = discriminabilities
true_model.assessment_offsets = difficulties
evaluate.training_auc(true_model, history, plot_roc_curve=True)

Construct a synthetic embedding


In [ ]:
num_students = 10000
num_assessment_interactions_per_step = 100
grid_size = 5
embedding_dimension = 2

num_assessments = grid_size ** 2
num_lessons = 2 * grid_size * (grid_size - 1)
num_lesson_interactions_per_student = 2 * (grid_size - 1) + 2

In [ ]:
S = np.zeros((num_students, embedding_dimension, num_lesson_interactions_per_student))
A = np.zeros((num_assessments, embedding_dimension))
L = np.zeros((num_lessons, embedding_dimension))
Q = np.zeros((num_lessons, embedding_dimension))

lesson_idx_of_loc = {}
assessment_idx_of_loc = {}

cell_size = 10 / (grid_size - 1)
lesson_count = 0
for i in xrange(grid_size):
    for j in xrange(grid_size):
        A[grid_size * i + j, :] = [i, j]
        assessment_idx_of_loc[(i, j)] = grid_size * i + j
        
        if j < grid_size - 1:
            Q[lesson_count, :] = [i, j]
            L[lesson_count, :] = [0, 1]
            lesson_idx_of_loc[(i, j, 0, 1)] = lesson_count
            lesson_count += 1
        
        if i < grid_size - 1:
            Q[lesson_count, :] = [i, j]
            L[lesson_count, :] = [1, 0]
            lesson_idx_of_loc[(i, j, 1, 0)] = lesson_count
            lesson_count += 1
            
A *= cell_size
Q *= cell_size
L *= cell_size

A = np.maximum(1e-3, A)
Q = np.maximum(1e-3, Q)

Sample interactions from the synthetic embedding


In [ ]:
id_of_loc = lambda x: '-'.join(str(z) for z in x)

In [ ]:
data = []
for student_idx in xrange(num_students):
    student_id = 'S' + str(student_idx)
    steps = ([(0, 1)] * (grid_size - 1)) +  ([(1, 0)] * (grid_size - 1))
    random.shuffle(steps)
    
    x, y = 0, 0
    t = 1
    assessment_idx = assessment_idx_of_loc[(0, 0)]
    assessment_id = id_of_loc(assessment_loc_of_idx[assessment_idx])
    pass_likelihood = 1 / (1 + math.exp(-(np.dot(S[student_idx, :, t], A[assessment_idx, :]) / np.linalg.norm(A[assessment_idx, :]) - np.linalg.norm(A[assessment_idx, :]))))
    outcome = random.random() < pass_likelihood
    data.append({
        'student_id' : student_id, 
        'module_id' : assessment_id,
        'module_type' : datatools.AssessmentInteraction.MODULETYPE,
        'timestep' : t,
        'outcome' : outcome})
    
    for i, j in steps:
        lesson_idx = lesson_idx_of_loc[(x, y, i, j)]
        lesson_id = id_of_loc(lesson_loc_of_idx[lesson_idx])
        data.append({
            'student_id' : student_id,
            'module_id' : lesson_id,
            'module_type' : datatools.LessonInteraction.MODULETYPE,
            'timestep' : t, 
            'outcome' : None})
        
        x += i
        y += j
        # DEBUG
        S[student_idx, :, t+1] = S[student_idx, :, t] + L[lesson_idx, :]# / (1 + math.exp(-(np.dot(S[student_idx, :, t], Q[lesson_idx, :]) / np.linalg.norm(Q[lesson_idx, :]) - np.linalg.norm(Q[lesson_idx, :]))))
        
        t += 1
        for _ in xrange(num_assessment_interactions_per_step):
            assessment_idx = random.randint(0, num_assessments - 1)
            assessment_id = id_of_loc(assessment_loc_of_idx[assessment_idx])
            pass_likelihood = 1 / (1 + math.exp(-(np.dot(S[student_idx, :, t], A[assessment_idx, :]) / np.linalg.norm(A[assessment_idx, :]) - np.linalg.norm(A[assessment_idx, :]))))
            outcome = random.random() < pass_likelihood
            # BEGIN DEBUG
            if assessment_idx_of_loc[(0, 0)] == assessment_idx:
                outcome = random.random() < 0.1
            # END DEBUG
            data.append({
                'student_id' : student_id,
                'module_id' : assessment_id,
                'module_type' : datatools.AssessmentInteraction.MODULETYPE,
                'timestep' : t,
                'outcome' : outcome})

In [ ]:
history = datatools.InteractionHistory(pd.DataFrame(data))

assessment_idx_map = {id_of_loc(loc): idx for idx, loc in assessment_loc_of_idx.iteritems()}
lesson_idx_map = {id_of_loc(loc): idx for idx, loc in lesson_loc_of_idx.iteritems()}
history.compute_idx_maps(assessment_idx=assessment_idx_map, lesson_idx=lesson_idx_map)

In [ ]:
len(history.data)

In [ ]:
history_path = os.path.join('data', 'lse_synthetic_history.pkl')

In [ ]:
with open(history_path, 'wb') as f:
    pickle.dump(history, f, pickle.HIGHEST_PROTOCOL)

Estimate an embedding from the sampled interactions


In [ ]:
model = models.EmbeddingModel(
    history, embedding_dimension=2, 
    using_lessons=True, using_prereqs=False, using_bias=True, 
    learning_update_variance_constant=0.5)

estimator = est.EmbeddingMAPEstimator(
    regularization_constant=1e-3, using_scipy=True, 
    debug_mode_on=True, ftol=1e-4)

model.fit(estimator)

In [ ]:
model = models.OneParameterLogisticModel(history.data, select_regularization_constant=True)
model.fit()

In [ ]:
evaluate.training_auc(model, history, plot_roc_curve=True)

Visualize the estimated embedding vs. the true embedding


In [ ]:
plt.scatter(A[:, 0], A[:, 1])
for assessment_idx in xrange(num_assessments):
    plt.annotate(id_of_assessment_idx(assessment_idx), (A[assessment_idx, 0], A[assessment_idx, 1]))
"""
for i in xrange(grid_size):
    for j in xrange(grid_size):
        if j < grid_size - 1:
            assessment_idxes = [assessment_idx_of_loc[(i, j)], assessment_idx_of_loc[(i, j + 1)]]
            plt.plot(A[assessment_idxes, 0], A[assessment_idxes, 1], c='black')
            
        if i < grid_size - 1:
            assessment_idxes = [assessment_idx_of_loc[(i, j)], assessment_idx_of_loc[(i + 1, j)]]
            plt.plot(A[assessment_idxes, 0], A[assessment_idxes, 1], c='black')
"""
plt.show()

In [ ]:
plt.scatter(model.assessment_embeddings[:, 0], model.assessment_embeddings[:, 1])
for assessment_idx in xrange(num_assessments):
    plt.annotate(id_of_assessment_idx(assessment_idx), (model.assessment_embeddings[assessment_idx, 0], model.assessment_embeddings[assessment_idx, 1]))
"""
for i in xrange(grid_size):
    for j in xrange(grid_size):
        if j < grid_size - 1:
            assessment_idxes = [assessment_idx_of_loc[(i, j)], assessment_idx_of_loc[(i, j + 1)]]
            plt.plot(model.assessment_embeddings[assessment_idxes, 0], model.assessment_embeddings[assessment_idxes, 1], c='black')
            
        if i < grid_size - 1:
            assessment_idxes = [assessment_idx_of_loc[(i, j)], assessment_idx_of_loc[(i + 1, j)]]
            plt.plot(model.assessment_embeddings[assessment_idxes, 0], model.assessment_embeddings[assessment_idxes, 1], c='black')
"""
plt.show()

In [ ]:
plt.quiver(Q[:, 0], Q[:, 1], L[:, 0], L[:, 1], pivot='tail', color='black')
"""
for i in xrange(grid_size):
    for j in xrange(grid_size):
        if j < grid_size - 1:
            lesson_idxes = [lesson_idx_of_loc[(i, j)], lesson_idx_of_loc[(i, j + 1)]]
            plt.plot(Q[lesson_idxes, 0], Q[lesson_idxes, 1], c='black')
            
        if i < grid_size - 1:
            lesson_idxes = [lesson_idx_of_loc[(i, j)], lesson_idx_of_loc[(i + 1, j)]]
            plt.plot(Q[lesson_idxes, 0], Q[lesson_idxes, 1], c='black')
"""
plt.xlabel('Skill 1')
plt.ylabel('Skill 2')
plt.xlim([-1, 11])
plt.ylim([-1, 11])
plt.show()

In [ ]:
plt.quiver(model.prereq_embeddings[:, 0], model.prereq_embeddings[:, 1], model.lesson_embeddings[:, 0], model.lesson_embeddings[:, 1], pivot='tail', color='black')
"""
for i in xrange(grid_size):
    for j in xrange(grid_size):
        if j < grid_size - 1:
            lesson_idxes = [lesson_idx_of_loc[(i, j)], lesson_idx_of_loc[(i, j + 1)]]
            plt.plot(model.prereq_embeddings[lesson_idxes, 0], model.prereq_embeddings[lesson_idxes, 1], c='black')
            
        if i < grid_size - 1:
            lesson_idxes = [lesson_idx_of_loc[(i, j)], lesson_idx_of_loc[(i + 1, j)]]
            plt.plot(model.prereq_embeddings[lesson_idxes, 0], model.prereq_embeddings[lesson_idxes, 1], c='black')
"""
plt.xlabel('Skill 1')
plt.ylabel('Skill 2')
plt.xlim([-1, 11])
plt.ylim([-1, 11])
plt.show()

In [ ]:
right_lesson_idxes = [lesson_idx_of_loc[(i, j, 1, 0)] for i in xrange(grid_size) for j in xrange(grid_size) if (i, j, 1, 0) in lesson_idx_of_loc]
up_lesson_idxes = [lesson_idx_of_loc[(i, j, 0, 1)] for i in xrange(grid_size) for j in xrange(grid_size) if (i, j, 0, 1) in lesson_idx_of_loc]

In [ ]:
plt.quiver(0, 0, L[right_lesson_idxes, 0], L[right_lesson_idxes, 1], pivot='tail', color='red', alpha=0.25)
plt.quiver(0, 0, L[up_lesson_idxes, 0], L[up_lesson_idxes, 1], pivot='tail', color='blue', alpha=0.25)

plt.xlabel('Skill 1')
plt.ylabel('Skill 2')
plt.xlim([-1, 11])
plt.ylim([-1, 11])
plt.show()

In [ ]:
plt.quiver(0, 0, model.lesson_embeddings[right_lesson_idxes, 0], model.lesson_embeddings[right_lesson_idxes, 1], pivot='tail', color='red', alpha=0.25)
plt.quiver(0, 0, model.lesson_embeddings[up_lesson_idxes, 0], model.lesson_embeddings[up_lesson_idxes, 1], pivot='tail', color='blue', alpha=0.25)

plt.xlabel('Skill 1')
plt.ylabel('Skill 2')
plt.xlim([-1, 11])
plt.ylim([-1, 11])
plt.show()

In [ ]:
plt.scatter(L[right_lesson_idxes, 0], L[right_lesson_idxes, 1], color='red', label='1-0')
plt.scatter(L[up_lesson_idxes, 0], L[up_lesson_idxes, 1], color='blue', label='0-1')
plt.xlabel('Skill 1')
plt.ylabel('Skill 2')
plt.legend(loc='best')
plt.show()

In [ ]:
plt.scatter(model.lesson_embeddings[right_lesson_idxes, 0], model.lesson_embeddings[right_lesson_idxes, 1], color='red', label='1-0')
plt.scatter(model.lesson_embeddings[up_lesson_idxes, 0], model.lesson_embeddings[up_lesson_idxes, 1], color='blue', label='0-1')
plt.xlabel('Skill 1')
plt.ylabel('Skill 2')
plt.legend(loc='best')
plt.show()

In [ ]:
student_idxes = random.sample(range(num_students), 10)

In [ ]:
for student_idx in student_idxes:
    plt.scatter(S[student_idx, 0, :], S[student_idx, 1, :], c='black')
    for i in xrange(num_lesson_interactions_per_student):
        plt.plot(S[student_idx, 0, i:(i+2)], S[student_idx, 1, i:(i+2)], c='black')
    plt.xlabel('Skill 1')
    plt.ylabel('Skill 2')
    plt.title('student_id = %s' % history.id_of_student_idx(student_idx))
    plt.show()

In [ ]:
for student_idx in student_idxes:
    plt.scatter(model.student_embeddings[student_idx, 0, :], model.student_embeddings[student_idx, 1, :], c='black')
    for i in xrange(num_lesson_interactions_per_student):
        plt.plot(model.student_embeddings[student_idx, 0, i:(i+2)], model.student_embeddings[student_idx, 1, i:(i+2)], c='black')
    plt.xlabel('Skill 1')
    plt.ylabel('Skill 2')
    plt.title('student_id = %s' % history.id_of_student_idx(student_idx))
    plt.show()

In [ ]:
for student_idx in student_idxes:
    for i in xrange(embedding_dimension):
        plt.plot(S[student_idx, i, :], '-s', label='Skill 1')
    plt.xlabel('Timestep')
    plt.ylabel('Skill')
    plt.title('student_id = %s' % history.id_of_student_idx(student_idx))
    plt.legend(loc='best')
    plt.show()

In [ ]:
for student_idx in student_idxes:
    for i in xrange(embedding_dimension):
        plt.plot(model.student_embeddings[student_idx, i, :], '-s', label='Skill 1')
    plt.xlabel('Timestep')
    plt.ylabel('Skill')
    plt.title('student_id = %s' % history.id_of_student_idx(student_idx))
    plt.legend(loc='best')
    plt.show()

In [ ]: