Lecture 7 - Demo Notebook¶

In this lecture, we will use the same data set as in lecture 6. We will create and interpret learning curves as well as compare the predictive performance of different approaches to knowledge tracing.

ASSISTments is a free tool for assigning and assessing math problems and homework. Teachers can select and assign problem sets. Once they get an assignment, students can complete it at their own pace and with the help of hints, multiple chances, and immediate feedback. Teachers get instant results broken down by individual student or for the whole class. The dataset involves 4,151 middle-school students practicing an electronic tutor that teaches and evaluates students in grade-school math. The student data are in a comma-delimited text file with one row per trial. The columns should correspond to a trial's user id, the order id (timestamp), the skill name, and and whether the student produced a correct response in the trial. More information on the platform can be found here.

The ASSISTments data sets are often used for benchmarking knowledge tracing models. We will play with a simplified data set that contains the following columns:

| Name | Description | | ---------------------- | ------------------------------------------------------------ | | user_id | The ID of the student who is solving the problem. | | | order_id | The temporal ID (timestamp) associated with the student's answer to the problem. | | | skill_name | The name of the skill associated with the problem. | | | correct | The student's performance on the problem: 1 if the problem's answer is correct at the first attempt, 0 otherwise.

We first load the data set.

In [1]:
%matplotlib inline
# Principal package imports
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
import scipy as sc

# Scikit-learn package imports
from sklearn import feature_extraction, model_selection
from sklearn.metrics import root_mean_squared_error, roc_auc_score

# PyBKT package imports
import random
old_randint = random.randint
random.randint = lambda a, b: old_randint(a, int(b))
from pyBKT.models import Model
random.randint = old_randint

# Import the lmm model class
from pymer4.models import Lmer

DATA_DIR = "./../../data/"
In [2]:
assistments = pd.read_csv(DATA_DIR + 'assistments.csv', low_memory=False).dropna()
assistments.head()
Out[2]:
user_id order_id skill_name correct
0 64525 33022537 Box and Whisker 1
1 64525 33022709 Box and Whisker 1
2 70363 35450204 Box and Whisker 0
3 70363 35450295 Box and Whisker 1
4 70363 35450311 Box and Whisker 0

Next, we print the number of unique students and skills in this data set.

In [3]:
print("Number of unique students in the dataset:", len(set(assistments['user_id'])))
print("Number of unique skills in the dataset:", len(set(assistments['skill_name'])))
Number of unique students in the dataset: 4151
Number of unique skills in the dataset: 110

We also implement a utility function that splits the data in two folds, making sure that all interactions of a student land in the same fold. We will use this function later when comparing predictive performance of the different models.

In [4]:
def create_iterator(data):
    '''
    Create an iterator to split interactions in data into train and test, with the same student not appearing in two diverse folds.
    :param data:        Dataframe with student's interactions.
    :return:            An iterator.
    '''    
    # Both passing a matrix with the raw data or just an array of indexes works
    X = np.arange(len(data.index)) 
    # Groups of interactions are identified by the user id (we do not want the same user appearing in two folds)
    groups = data['user_id'].values 
    return model_selection.GroupShuffleSplit(n_splits=1, train_size=.8, test_size=0.2, random_state=0).split(X, groups=groups)

BKT - Learning Curves¶

Last week, we have seen how to use BKT to predict the probability that a student will solve a task correctly. In addition, we can also use this type of model to compute learning curves and in this way analyze the learning activity (in our case the skills).

We will compute learning curves for the following six skills:
'Circle Graph', 'Venn Diagram', 'Mode', 'Division Fractions', 'Finding Percents', 'Area Rectangle'

In [5]:
skills_subset = ['Circle Graph', 'Venn Diagram', 'Mode', 'Division Fractions', 'Finding Percents', 'Area Rectangle']
data = assistments[assistments['skill_name'].isin(skills_subset)]

print("Skill set:", set(data['skill_name']))
print("Number of unique students in the subset:", len(set(data['user_id'])))
print("Number of unique skills in the subset:", len(set(data['skill_name'])))
Skill set: {'Finding Percents', 'Mode', 'Circle Graph', 'Area Rectangle', 'Venn Diagram', 'Division Fractions'}
Number of unique students in the subset: 1527
Number of unique skills in the subset: 6

We first fit a BKT model with all default parameters, i.e., Model(seed=0) in pyBKT, on the full data data set (no split into train and test set needed as we are not assessing predictive performance of the model here, but just checking interpretation).

In [6]:
# Initialize the model
model = Model(seed=0)

# Fit the model on the entire dataset
%time model.fit(data=data)

predictions = model.predict(data=data)[['user_id', 'skill_name', 'correct', 'correct_predictions']]

# Rename the dataframe columns as per instructions
predictions.columns = ['user_id', 'skill_name', 'y_true', 'y_pred_bkt']
CPU times: user 60.8 ms, sys: 6.57 ms, total: 67.4 ms
Wall time: 77.3 ms
In [7]:
predictions.head()
Out[7]:
user_id skill_name y_true y_pred_bkt
3957 14 Circle Graph 0 0.50000
3958 14 Circle Graph 1 0.00000
3959 14 Circle Graph 0 0.00000
3960 14 Circle Graph 0 0.00000
3961 14 Circle Graph 0 0.00000

Next, we create a function that computes the learning curve (observed or predicted) for us by averaging over the success rate of all users at a given opportunity.

In [8]:
def avg_y_by_x(x, y):
    '''
    Compute average learning curve and number of students over the number of opportunities. 
    x is the number of opportunities.
    y the success rates of the users (can be predicted success rate or true success rate).
    '''
    # Transform lists into arrays
    x = np.array(x)
    y = np.array(y)

    # Sort the integer id representing the number of opportunities in increasing order
    xs = sorted(list(set(x)))

    # Supporting lists to store the:
    # - xv: integer identifier of the number of opportunities
    # - yv: average value across students at that number of opportunities
    # - lcb and ucb: lower and upper confidence bound
    # - n_obs: number of observartions present at that number of opportunities (on per-skill plots, it is the #students)
    xv, yv, lcb, ucb, n_obs = [], [], [], [], []

    # For each integer identifier of the number of opportunities 0, ...
    for v in xs:
        ys = [y[i] for i, e in enumerate(x) if e == v] # We retrieve the values for that integer identifier
        if len(ys) > 0: 
            xv.append(v) # Append the integer identifier of the number of opportunities
            yv.append(sum(ys) / len(ys)) # Append the average value across students at that number of opportunities
            n_obs.append(len(ys)) # Append the number of observartions present at that number of opportunities

            
            # Prepare data for confidence interval computation
            unique, counts = np.unique(ys, return_counts=True)
            counts = dict(zip(unique, counts))

            if 0 not in counts:
                counts[0] = 0
            if 1 not in counts:
                counts[1] = 0

            # Calculate the 95% confidence intervals
            ci = sc.stats.beta.interval(0.95, 0.5 + counts[0], 0.5 + counts[1])
            lcb.append(ci[0])
            ucb.append(ci[1])

    return xv, yv, lcb, ucb, n_obs

Then, we create a function for plotting learning curve and a bar chart with the number of students per opportunity for a given skill.

In [9]:
def plot_learning_curve(skill_name):
    '''
    Plot learning curve using BKT model for skill `skill_name`. 
    '''
    preds = predictions[predictions['skill_name'] == skill_name] # Retrieve predictions for the current skill

    xp = []
    yp = {}
    for col in preds.columns: # For y_true and and y_pred_bkt columns, initialize an empty list for curve values
        if 'y_' in col:
            yp[col] = []

    for user_id in preds['user_id'].unique(): # For each user
        user_preds = preds[preds['user_id'] == user_id] # Retrieve the predictions on the current skill for this user 
        xp += list(np.arange(len(user_preds))) # The x-axis values go from 0 to |n_opportunities|-1
        for col in preds.columns: 
            if 'y_' in col: # For y_true and and y_pred_bkt columns
                yp[col] += user_preds[col].tolist() # The y-axis value is the success rate for this user at that opportunity

    fig, axs = plt.subplots(2, 1, gridspec_kw={'height_ratios': [3, 2]}) # Initialize the plotting figure

    lines = []
    for col in preds.columns:
        if 'y_' in col: # For y_true and and y_pred_bkt columns   
            x, y, lcb, ucb, n_obs = avg_y_by_x(xp, yp[col]) # Calculate mean and 95% confidence intervals for success rate 
            y = [1-v for v in y] # Transform success rate in error rate
            if col == 'y_true': # In case of ground-truth data, we also show the confidence intervals
                axs[0].fill_between(x, lcb, ucb, alpha=.1)
            model_line, = axs[0].plot(x, y, label=col) # Plot the curve
            lines.append(model_line) # Store the line to then set the legend    

    # Make decorations for the learning curve plot
    axs[0].set_title(skill_name)
    axs[0].legend(handles=lines)
    axs[0].set_ylabel('Error')
    axs[0].set_ylim(0, 1)
    axs[0].set_xlim(0, None)

    # Plot the number of observations per number of opportunities bars and make decorations
    axs[1].set_xlabel('#Opportunities')
    axs[1].bar([i for i in range(len(n_obs))], n_obs)
    axs[1].set_ylabel('#Observations')
    axs[1].set_ylim(0, 750)
    axs[1].set_xlim(0, None)

    # Plot the learning curve and the bar plot 
    plt.show()

Finally, we display and interpret the learning curves for the six selected skills.

In [10]:
for skill_name in skills_subset[:3]: # For the first three skills under consideration
    plot_learning_curve(skill_name)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

We first discuss the first three skills separately. Then, given that we are showing the same aspects for the six skills, we provide few more observations aimed at comparing the findings we obtained across the skills under consideration.

  • "Circle Graph". From the error rate in the ground-truth data (y_true, blue), it can be observed that this skill appears quite hard for students, with an initial error rate of around 0.60 in the first opportunity. The error rate goes slightly down, as we would usually expect, as much as the students play with the skill, reaching the lowest error rate of 0.38 at around 7 opportunities. However, starting from #opportunities around equal to 7, the error rate starts going up till 0.80 after 50 opportunities. It seems that a large part of the students managed to master a bit this skill in the first opportunities, but then they start facing difficulties in mastering it. This behavior might be due to the fact that the skill is ill-defined or that the problems become too difficult or are not aligned well with the skill. After 50 opportunities, the error rate starts jumping between 0 and a 1 just because there are only few students. In terms of confidence interval (blue area), it can be observed that the error rate is quite stable at the earlier stage. Starting from around 7 opportunities, the error rate is less stable probably due to the same reasons we provided above. The confidence interval finally becomes large when only few students keep playing with this skill for a higher number of opportunities. When it comes to consider the error rate obtained by the BKT model estimations (y_pred_bkt, orange), it can be observed that the model tends to overestimate a bit the error rate during the first opportunities (the orange line is above the blue one, the model tends to predict more errors than expected), while the opposite pattern is observed for higher numbers of opportunities (the model tends to predict less errors than expected). However, in general, the model fits well the ground-truth data. Looking at the bar plot at the bottom, students appear reasonably engaged with this skill only for few opportunities (particularly, starting from 35 opportunities, the number of involved students is low, and the error rate starts jumping more).

  • "Venn Diagram". This third skill appears like another somehow easy skill since the beginning (y_true, blue), but a bit more harder than "Mode". Except for a few fluctuations in the very first opportunities (where the curve slightly increases, maybe because they are just the first opportunities or due to the type/difficulty of problems proposed at that point), the error rate goes down till around 0.08, reached at around 100 opportunities. Probably, once only those students struggling more with this skill tend to stay for a higher number of opportunities, the error rate goes up again. The confidence interval for the ground-truth data (blue area) is relatively high (if compared with the one observed for the other two skills during the first opportunities). Naturally, it increases even more when the number of students reaching that number of opportunities is very low (#opportunities > 130). Comparing the ground-truth estimations with the BKT estimations (y_pred_bkt, orange), except for the first opportunities, it can be observed that BKT tends to consistently underestimate the error rate and the BKT error rate is often outside the confidence interval of actual data. The number of students per opportunity naturally goes down as observed for the other skills, but interestingly it can be observed that a set of students seems to still keep playing with this skill from around 30 opportunites till 100 opportunities stably (the bar plot values appear somehow similar in that range).

  • "Mode". Based on the patterns of the ground-truth data (y_true, blue), the error rate observed for this skill at the earlier stages is of around 0.20. The students thus are already good in problems involving this skill, somehow. While this might depend on the intrinsic knowledge/background of the students about this skill, this behavior might also appear due to the fact that the skill is easier and easier to master in general or that the problems presented to the students on that skill are too easy, for instance. While the error rate goes slightly down when the number of opportunities increases, the difference between the initial error rate and the error rate experienced (for instance) after 5 or 6 opportunities is relatively small (around 0.10 of error rate in the latter case). This observation might be justified by the fact that students played with the skill only for few opportunities (so there is no so much room for improving more) and by the already low initial error rate (students might use those opportunities just to refine their knowledge, with few errors still happening). The going-up behavior at the end appears mainly due to the very low number of students involved at that point. The blue area shows us that the confidence interval is very small for the first three or four opportunities (so behavior is more consistent among students), while it starts increasing later probably also due to the peculiar patterns behind students who interact more with this skill. The BKT model (y_pred_bkt, orange) is able to fit the ground-truth data very well across the number of opportunities, especially till around 8 opportunities. One important observation comes from the bar plot, which shows that the number of students involved in this skill drastically goes down especially after three/four opportunities.

Overall, comparing the first three skills, "Circle Graph" seems to be really going up (e.g., ill-defined skill or due to the type/difficulty of problems), while, for "Mode" and "Venn Diagram", the going up later seems to come only from the very low number of students. Especially but not only for the "Venn Diagram" skill, the BKT generally tends to under-estimate the error rate. One reason behind this observation could be that the BKT model we adopted did not consider forgetting, for instance. In terms of number of opportunities, students experienced a lower maximum number of opportunities on the "Mode" skill, compared to the others. Compared to "Circle Graph", the skill "Venn Diagram" seems to involve lots of students even for higher number of opportunities (e.g., #opportunities > 30).

In [11]:
for skill_name in skills_subset[3:]: # For the remaining three skills under consideration    
    plot_learning_curve(skill_name)
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

We briefly discuss the remaining three learning curves.

  • "Division Fractions". With a 40% error rate in the ground truth curve (y_true, blue), we can see that this skill is generally moderately difficult for students (not very easy, but the majority of the students correctly solve the problem on the first attempt). The error rate decreases slightly with no major jumps as the number of opportunities increases, which means that 1) the problems in the skill are close to equally difficult and 2) more opportunities leads to students improving on the problem (lower error rate). The y_pred_bkt learning curve (orange) for this skill is close to straight and slightly downward trending from 40% to 20%, which shows that students improve on the problem the more attempts that are made. However, the BKT model fails to capture the 0% error rate at 30 opportunities, likely due to the few number of data points around 25/30 observations.

  • "Finding Percents". We identify that this skill is difficult for the majority of students because 60% of students do not pass this problem on the first try. We see a downwards curve in error rate in the ground truth and BKT predictions from 0 to 20 opportunities, where the error drops from 60% to 40%. However, after that, we see a steading increase until 50 opportunties, which shows that if a student has over 20 opportunities in this skill, they are not likely to pass it even until 50 opportunities -- more opportunities does not help. We note very odd behavior of the learning curves between 50 and 80, which can be directly attributed to a small number of observational data points (examining the bottom bar plot).

  • "Area Rectangle". Students find the problems in this skill very easy to solve, with an initial error rate of 20% across both the ground truth and the BKT predictive curves, and steadily dropping overall. One interesting trend is between 0 to 60 opportunities, we do not see a notable decrease in error rate, which leads us to say that up to the 50 first opportunities do not make a significant difference in average student's aptitude on the skill. Between 60 and 100, we see a notable drop to an error rate of 10% and then another step, where the difference between 60 and 100 opportunities does not change the error rate. Around 125 opportunities, the error rate flatlines to a very small percentage, showing that most students have mastered the skill. This step-wise learning curve could be indicative of a quirk in the curriculum, and we would recommend further analysis of the problem distribution to understand why this appears. It is notable that the prediction learning curve (orange) fails to capture the peaks in student learning rate between 175 and 300, likely due to the low numbers of opportunities.

Examining all 6 skills, we see that Circle Graphs and Finding Percents have similar behavior as difficult problems, Venn Diagram and Area Rectangle have similar behavior as easier problems, and the remaining two skills have unique patterns.

Additive Factors Model (AFM) and Performance Factors Analysis (PFA)¶

The AFM and PFA models are both based on logistic regression and item response theory (IRT). Specifically, they compute the probability that a student will solve a task correctly based on the number of previous attempts the student had at the corresponding skill (in case of AFM) and based on the correct and wrong attempts at the corresponding skill (in case of PFA), respectively. We therefore first preprocess the data to compute these variables. For demonstration purposes, we will continue on the small subset of the data set containing six skills.

In [12]:
# Data processing
# Number of attempts before current
def preprocess_data(data):
    data['aux'] = 1
    data['prev_attempts'] = data.sort_values('order_id').groupby(['user_id', 'skill_name'])['aux'].cumsum() -1

    # Number of correct and incorrect attempts before current attempt
    data['correct_aux'] = data.sort_values('order_id').groupby(['user_id', 'skill_name'])['correct'].cumsum()
    data['before_correct_num'] = data.sort_values('order_id').groupby(['user_id', 'skill_name'])['correct_aux'].shift(periods=1, fill_value=0)
    data['before_wrong_num'] = data['prev_attempts'] - data['before_correct_num']
    return data

data = preprocess_data(data)
data.head()
Out[12]:
user_id order_id skill_name correct correct_predictions state_predictions aux prev_attempts correct_aux before_correct_num before_wrong_num
3957 14 21617623 Circle Graph 0 0.50000 0.01498 1 0 0 0 0
3958 14 21617632 Circle Graph 1 0.00000 0.00000 1 1 1 0 1
3959 14 21617641 Circle Graph 0 0.00000 0.00000 1 2 1 1 1
3960 14 21617650 Circle Graph 0 0.00000 0.00000 1 3 1 1 2
3961 14 21617659 Circle Graph 0 0.00000 0.00000 1 4 1 1 3

Next, we split the data into a training and a test data set.

In [13]:
# Obtain indexes
train_index, test_index = next(create_iterator(data))
# Split the data
X_train, X_test = data.iloc[train_index], data.iloc[test_index]

Next, we fit an AFM model to the training data and predict on the test data. Note that the implementation below only works for a one-to-one correspondance of task and skill, i.e. when a task is associated to exactly one skill. In case of a data set containing tasks with multiple skills, we would need to use the pyAFM package. A tutorial on using pyAFM can be found here.

In [14]:
# Initialize and fit the model
model = Lmer("correct ~ (1|user_id) + (1|skill_name) + (0 + prev_attempts|skill_name)", data=X_train, family='binomial')
%time model.fit() 
# Compute predictions
X_test['afm_predictions'] = model.predict(data=X_test, verify_predictions=False)
X_test.head()
Linear mixed model fit by maximum likelihood  ['lmerMod']
Formula: correct~(1|user_id)+(1|skill_name)+(0+prev_attempts|skill_name)

Family: binomial	 Inference: parametric

Number of observations: 40258	 Groups: {'user_id': 1221.0, 'skill_name': 6.0}

Log-likelihood: -16797.782 	 AIC: 33603.565

Random effects:

                       Name     Var     Std
user_id         (Intercept) 2.56000 1.60000
skill_name      (Intercept) 0.68300 0.82700
skill_name.1  prev_attempts 0.00500 0.06900

No random effect correlations specified

Fixed effects:

CPU times: user 17 s, sys: 4.89 s, total: 21.9 s
Wall time: 30.5 s
Out[14]:
user_id order_id skill_name correct correct_predictions state_predictions aux prev_attempts correct_aux before_correct_num before_wrong_num afm_predictions
53382 53167 26451283 Mode 1 0.50000 0.00105 1 0 1 0 0 0.85507
53383 53167 26451302 Mode 1 0.00000 0.00000 1 1 2 1 0 0.86928
157253 53167 32517498 Division Fractions 0 0.50000 0.00905 1 0 0 0 0 0.62272
157254 53167 32517627 Division Fractions 1 0.00000 0.00000 1 1 1 0 1 0.64312
157255 53167 32517648 Division Fractions 1 0.00000 0.00000 1 2 2 1 1 0.66302

Next, we will plot the performance metrics (RMSE and AUC) for the model.

In [15]:
def plot_metrics(df_preds, model):

    # Compute RMSE and AUC per skill
    skill_rmse = df_preds.groupby(['skill_name']).apply(lambda df_preds: root_mean_squared_error(df_preds.correct, df_preds[model]), include_groups=False)
    print('RMSE', np.mean(skill_rmse), np.std(skill_rmse))

    skill_auc = df_preds.groupby(['skill_name']).apply(lambda df_preds: roc_auc_score(df_preds.correct, df_preds[model]), include_groups=False)
    print('AUC', np.mean(skill_auc), np.std(skill_auc))

    # Create overall RMSE and RMSE per skill data frames
    skills_all = ['Skills']*len(skills_subset)
    df_overall_rmse = pd.DataFrame(list(zip(skills_all, skill_rmse)),
                   columns =['x', 'RMSE'])
    df_skill_rmse = pd.DataFrame(list(zip(skills_subset, skill_rmse)),
                   columns =['x', 'RMSE'])

    # Create overall AUC and AUC per skill data frames
    df_overall_auc = pd.DataFrame(list(zip(skills_all, skill_auc)),
                   columns =['x', 'AUC'])
    df_skill_auc = pd.DataFrame(list(zip(skills_subset, skill_auc)),
                   columns =['x', 'AUC'])

    # Two bar plots for RMSE: first one with std (mean RMSE over all skills), then one bar plot with a bar for each specific skill
    fig, axes = plt.subplots(1, 2, figsize=(10,5))
    sns.barplot(ax=axes[0],x='x', y='RMSE', data=df_overall_rmse, estimator=np.mean, errorbar='sd')
    axes[0].set_title('Overall RMSE')
    axes[0].set_xlabel('')

    sns.barplot(ax=axes[1],x='x', y='RMSE', data=df_skill_rmse, estimator=np.mean, errorbar='sd')
    plt.xticks(rotation=90)
    axes[1].set_title('RMSE across skills')
    axes[1].set_xlabel('')

    plt.show()

    # Two bar plots for AUC: first one with std (mean AUC over all skills), then one bar plot with a bar for each specific skill
    fig, axes = plt.subplots(1, 2, figsize=(10,5))
    sns.barplot(ax=axes[0],x='x', y='AUC', data=df_overall_auc, estimator=np.mean, errorbar='sd')
    axes[0].set_title('Overall AUC')
    axes[0].set_xlabel('')

    sns.barplot(ax=axes[1],x='x', y='AUC', data=df_skill_auc, estimator=np.mean, errorbar='sd')
    plt.xticks(rotation=90)
    axes[1].set_title('AUC across skills')
    axes[1].set_xlabel('')

    plt.show()
In [16]:
plot_metrics(X_test, 'afm_predictions')
RMSE 0.4187859248639723 0.06896549818413097
AUC 0.6051593380658353 0.06187187963009105
No description has been provided for this image
No description has been provided for this image

Similar to last week, we observe that the RMSE and AUC are dependent on the skill, i.e. performance varies a lot across skills.

Next, we fit a PFA model to the data. Again, this implementation works for one-to-one correspondance and tasks with multiple skills would require the use of pyAFM.

In [17]:
# Initialize and fit the model
model = Lmer("correct ~ (1|user_id) + (1|skill_name) + (0 + before_correct_num|skill_name) + (0 + before_wrong_num|skill_name)", data=X_train, family='binomial')
%time model.fit() 
# Compute predictions
X_test['pfa_predictions'] = model.predict(data=X_test, verify_predictions=False)
X_test.head()
Linear mixed model fit by maximum likelihood  ['lmerMod']
Formula: correct~(1|user_id)+(1|skill_name)+(0+before_correct_num|skill_name)+(0+before_wrong_num|skill_name)

Family: binomial	 Inference: parametric

Number of observations: 40258	 Groups: {'user_id': 1221.0, 'skill_name': 6.0}

Log-likelihood: -16385.969 	 AIC: 32781.939

Random effects:

                            Name     Var     Std
user_id              (Intercept) 1.74800 1.32200
skill_name           (Intercept) 0.69900 0.83600
skill_name.1  before_correct_num 0.02600 0.16200
skill_name.2    before_wrong_num 0.00000 0.01000

No random effect correlations specified

Fixed effects:

CPU times: user 31.3 s, sys: 4.24 s, total: 35.5 s
Wall time: 49.4 s
Out[17]:
user_id order_id skill_name correct correct_predictions state_predictions aux prev_attempts correct_aux before_correct_num before_wrong_num afm_predictions pfa_predictions
53382 53167 26451283 Mode 1 0.50000 0.00105 1 0 1 0 0 0.85507 0.83721
53383 53167 26451302 Mode 1 0.00000 0.00000 1 1 2 1 0 0.86928 0.87290
157253 53167 32517498 Division Fractions 0 0.50000 0.00905 1 0 0 0 0 0.62272 0.61971
157254 53167 32517627 Division Fractions 1 0.00000 0.00000 1 1 1 0 1 0.64312 0.62047
157255 53167 32517648 Division Fractions 1 0.00000 0.00000 1 2 2 1 1 0.66302 0.65994

Next, we also plot the performance metrics (RMSE and AUC) for the model.

In [18]:
plot_metrics(X_test, 'pfa_predictions')
RMSE 0.40175375765684646 0.06679636660411878
AUC 0.7263985371288011 0.09235565673977136
No description has been provided for this image
No description has been provided for this image

Again, performance of the model varies across skills. In terms of pattern across the skills, PFA is similar to AFM.

Model Comparison on Subset¶

Next, we compare the predictive performance of all knowledge tracing models we have seen so far on our subset of data containing six skills.

We first also fit a BKT model to this data set using the same train/test split as above.

In [19]:
df_preds = pd.DataFrame()

# Train a BKT model for each skill
for skill in skills_subset:
    print("--{}--".format(skill))
    X_train_skill = X_train[X_train['skill_name'] == skill]
    X_test_skill = X_test[X_test['skill_name'] == skill]
    # Initialize and fit the model
    model = Model(seed=0)
    %time model.fit(data=X_train_skill) 
    preds = model.predict(data=X_test_skill) [['user_id', 'order_id', 'skill_name', 'correct', 'prev_attempts',
       'before_correct_num', 'before_wrong_num', 'afm_predictions', 'pfa_predictions', 'correct_predictions']]
    df_preds = pd.concat([df_preds, preds], ignore_index=True)

X_test = df_preds
X_test.columns = ['user_id', 'order_id', 'skill_name', 'correct', 'prev_attempts',
       'before_correct_num', 'before_wrong_num', 'afm_predictions', 'pfa_predictions', 'bkt_predictions']
X_test = X_test.dropna(subset=['afm_predictions', 'pfa_predictions', 'bkt_predictions'])
X_test.head()
--Circle Graph--
CPU times: user 8.02 ms, sys: 2.58 ms, total: 10.6 ms
Wall time: 16.7 ms
--Venn Diagram--
CPU times: user 15.8 ms, sys: 2.95 ms, total: 18.8 ms
Wall time: 33.4 ms
--Mode--
CPU times: user 3.89 ms, sys: 68 μs, total: 3.96 ms
Wall time: 4.08 ms
--Division Fractions--
CPU times: user 5.35 ms, sys: 320 μs, total: 5.67 ms
Wall time: 6.64 ms
--Finding Percents--
CPU times: user 8.2 ms, sys: 596 μs, total: 8.8 ms
Wall time: 9.91 ms
--Area Rectangle--
CPU times: user 9.98 ms, sys: 587 μs, total: 10.6 ms
Wall time: 11.6 ms
Out[19]:
user_id order_id skill_name correct prev_attempts before_correct_num before_wrong_num afm_predictions pfa_predictions bkt_predictions
0 64525 28186893 Circle Graph 1 0 0 0 0.48266 0.46224 0.50000
1 64525 28187093 Circle Graph 1 1 1 0 0.49251 0.48999 0.00000
2 64525 32413158 Circle Graph 1 2 2 0 0.50236 0.51780 0.00000
3 64525 33022751 Circle Graph 0 3 3 0 0.51221 0.54551 0.00000
4 64525 33023039 Circle Graph 1 4 3 1 0.52205 0.54598 0.00000

Finally, we plot the overall RMSE and overall AUC of all models.

In [20]:
rmse_bkt = root_mean_squared_error(X_test['bkt_predictions'],X_test['correct'])
rmse_afm = root_mean_squared_error(X_test['afm_predictions'],X_test['correct'])
rmse_pfa = root_mean_squared_error(X_test['pfa_predictions'],X_test['correct'])

rmse = [rmse_bkt, rmse_afm, rmse_pfa]
models = ['BKT', 'AFM', 'PFA']

plt.bar(models, rmse)
plt.ylabel('RMSE')
Out[20]:
Text(0, 0.5, 'RMSE')
No description has been provided for this image
In [21]:
auc_bkt = roc_auc_score(X_test['correct'], X_test['bkt_predictions'])
auc_afm = roc_auc_score(X_test['correct'], X_test['afm_predictions'])
auc_pfa = roc_auc_score(X_test['correct'], X_test['pfa_predictions'])

auc = [auc_bkt, auc_afm, auc_pfa]
models = ['BKT', 'AFM', 'PFA']

plt.bar(models, auc)
plt.ylabel('AUC')
Out[21]:
Text(0, 0.5, 'AUC')
No description has been provided for this image

We observe that BKT outperforms AFM and PFA in terms of RMSE and AUC. Not unexpectedly, PFA is performing better than AFM.

In [ ]: