Download this notebook from SiddharthaPradhan/stanbkt

Simple Example using Simulated Data#

To get started with StanBKT, use this example to explore the following concepts:

  1. How to simulate data using sim_simple_BKT

  2. How to instantiate a model for MCMC fitting.

  3. How to specify priors and stan compile arguments.

  4. How to fit the model to the data

Simulate BKT data#

The data generation process is based on the standard BKT model with population wide parameters.

[1]:
from stanbkt.utils import sim_simple_BKT
[2]:
# define ground truth BKT parameters for data simulation
N_KCS = 2  # we will simulate data for 2 KCs
bkt_params = {  # define BKT parameters for each KCs
    "prior": [0.4, 0.1],
    "learn": [0.04, 0.08],
    "forget": [0.01, 0.005],
    "guess": [0.1, 0.3],
    "slip": [0.05, 0.05],
}
[3]:
data_df = sim_simple_BKT(
    n_students=30,  # 30 students
    n_problems=60,  # 60 problems
    n_kcs=2,  # 2 knowledge components
    frac=0.8,  # sample 80% of generated data (simulates missing data)
    rng_seed=1234,  # random seed for reproducibility
    **bkt_params,  # use the defined BKT parameters for data simulation
)

StanBKT expects data in a long format with four required columns:

  1. Student ID: ID of the student

  2. Problem ID: ID of the problem

  3. Correctness: A binary indicator (1/0) of whether the student answered the problem correctly

  4. Problem Order: The order in which a student attempted the problems (e.g., a timestamp)

If KC ID is not included in the DataFrame, StanBKT assumes all problems belong to the same KC.

[4]:
data_df.head(10)
[4]:
student_id problem_id correct timestamp kc_id
0 stu_0 prob_0 0 2024-01-01 00:00:00 kc_1
1 stu_0 prob_1 0 2024-01-01 00:01:00 kc_1
2 stu_0 prob_2 0 2024-01-01 00:02:00 kc_1
3 stu_0 prob_4 0 2024-01-01 00:04:00 kc_0
4 stu_0 prob_6 0 2024-01-01 00:06:00 kc_0
5 stu_0 prob_7 1 2024-01-01 00:07:00 kc_0
6 stu_0 prob_8 1 2024-01-01 00:08:00 kc_0
7 stu_0 prob_9 1 2024-01-01 00:09:00 kc_0
8 stu_0 prob_11 1 2024-01-01 00:11:00 kc_0
9 stu_0 prob_13 1 2024-01-01 00:13:00 kc_0

Additionally, if column names differ from the expected names, StanBKT requires a mapping from expected column names to the actual DataFrame column names.

[5]:
from stanbkt.utils import ColumnNames

# define column mapping for the data
# this will be used in subsequent calls such as model fitting and prediction
col_mapping = {
    ColumnNames.STUDENT_ID: "student_id",
    ColumnNames.PROBLEM_ID: "problem_id",
    ColumnNames.KC_ID: "kc_id",
    ColumnNames.CORRECTNESS: "correct",
}

Define model#

[6]:
from stanbkt.models import StandardBKT
from stanbkt.fits import FitMethod
from stanbkt.utils import VerbosityLevel

Defining the Model#

The following code block creates a StandardBKT model (which includes the Forgetting parameter), that will be fit using MCMC.

[7]:
model = StandardBKT(
    fit_method=FitMethod.MCMC,  # use MCMC for parameter estimation
    verbose=VerbosityLevel.WARN,  # only print warnings
)

Fitting the Model#

StanBKT compiles the underlying Stan code lazily on the first fit call. Which means calling model.fit(...) for the first time will first compile the model and cache it in the platform specific cache directory (e.g. .cache on Linux). Instantiating a model with the same type (i.e. Standard, Grouped, etc.), stan_compile_kwargs and cpp_compile_kwargs will use the previously compiled model. See :ref: xyz for more information.

We can fit the model passing data for each KC individually or as a whole. Subsequently calling fit will not remove previously fitted KCs, instead it will add additional fitted KCs to the model.

Fitting each KCs individually is particularly useful when we need different bayesian priors and stan fit options. Note. each fit method (i.e. MCMC, Variation Inference, Pathfinder or MLE) has different fit options (see :ref: xyz).

In this example, the default priors and MCMC fit options is used for for kc_1 and custom priors and options for kc_2.

[8]:
kc_0_df = data_df[data_df["kc_id"] == "kc_0"]
# fit the model to the data for kc_0, using default priors and default MCMC settings
model.fit(kc_0_df)
11:14:57 - cmdstanpy - INFO - CmdStan start processing

11:14:59 - cmdstanpy - INFO - CmdStan done processing.

[8]:
StandardBKT(fit_method=<FitMethod.MCMC: 'mcmc'>, verbose=<VerbosityLevel.WARN: 1>, is_fitted=True)

We define the Bayesian priors for kc_2 based on domain knowledge and uncertainty in that knowledge. These priors are on the logit scale and are modeled as normal distributions. The inverse logit function \(f(x) = \frac{1}{1+e^{-x}}\), transforms the logits into the probability scale.

It is important to note that the guess and slip parameters are on the half-logit scale, i.e. these parameters have a maximum value of 0.5 on the probability scale. This is done to ensure identifiability and prevent model degeneracy (see :ref: xyz).

Hence, for either the learn or forget parameter, a prior of \(\mathcal{N}(0, 2)\) would correspond to a prior mean probability of 0.5 and a 95% prior probability values between 0.0194 and 0.980.

However, for either the guess and slip parameters, a prior of \(\mathcal{N}(0, 2)\) would correspond to a prior mean probability of 0.25 and a 95% prior probability values between 0.0097 and 0.49. Again, this is due to the fact that guess and slip parameters are on the half-logit scale.

Any parameter without specified priors will use the default priors, alternatively, one can choose to use no priors i.e., improper non-informative prior, which is modeled as a uniform distribution over the parameter space. To do this, explicitly set the parameters as None e.g. BayesianPriors(pi_know_mu=None, pi_know_std=None), or pass in use_defaults=False, which initializes all non-specified parameters as None.

[9]:
from stanbkt.models import StandardPriors

# define bayesian priors for prior knowledge and guess parameters
# any parameters not specified here will use the default priors
# to use Improper non-informative priors, i.e., uniform distribution over the parameter space,
# explicitly pass None, or pass in use_defaults=False in the StandardPriors constructor (i.e. StandardPriors(use_defaults=False))
priors_kc_1 = StandardPriors(
    pi_know_mu=0,
    pi_know_std=2,  # prior for initial knowledge (pi_know)
    guess_mu=0,
    guess_std=2,  # prior for guess parameter
)
[10]:
from stanbkt.fits import MCMCFitOptions

fit_opts = MCMCFitOptions(
    seed=1234,  # seed for reproducibility
    iter_warmup=500,  # number of warmup iterations for MCMC
    iter_sampling=500,  # number of sampling iterations for MCMC
)
[11]:
kc_1_df = data_df[data_df["kc_id"] == "kc_1"]
# fit the model to the data for kc_1, using the defined priors and MCMC settings
model.fit(kc_1_df, stan_fit_options=fit_opts, priors=priors_kc_1)
11:14:59 - cmdstanpy - INFO - CmdStan start processing

11:15:01 - cmdstanpy - INFO - CmdStan done processing.

[11]:
StandardBKT(fit_method=<FitMethod.MCMC: 'mcmc'>, verbose=<VerbosityLevel.WARN: 1>, is_fitted=True)

Alternatively, the entire dataframe can be passed to fit the model all Kcs in the data model.fit(data_df, ...)

[12]:
model.summary()
[12]:
Mean MCSE StdDev MAD 2.5% 50% 97.5% ESS_bulk ESS_tail R_hat
kc_id parameter
kc_0 lp__ -236.912000 0.036365 1.604140 1.422550 -240.863000 -236.587000 -234.814000 1999.530 2733.78 1.001580
logit_pi_know_group[1] -0.445610 0.006437 0.441265 0.432285 -1.306560 -0.448010 0.430112 4702.520 3163.94 0.999969
logit_learn_group[1] -2.711160 0.005143 0.322566 0.320049 -3.407340 -2.693420 -2.142760 4231.740 2562.92 1.000380
logit_forget_group[1] -4.588980 0.009253 0.576608 0.568585 -5.859910 -4.547700 -3.578490 4207.500 2724.21 1.001550
logit_guess_group[1] -1.604210 0.004213 0.287789 0.290034 -2.208330 -1.593260 -1.077050 4795.970 2804.12 1.000670
logit_slip_group[1] -2.180530 0.004495 0.285021 0.269611 -2.779010 -2.158010 -1.662600 4452.090 2495.93 1.001170
pi_know[1] 0.395075 0.001466 0.101158 0.101823 0.213063 0.389834 0.605900 4702.540 3163.94 0.999969
learn[1] 0.064915 0.000281 0.018786 0.018746 0.032067 0.063363 0.105010 4231.740 2562.92 1.000360
forget[1] 0.011653 0.000092 0.006222 0.005670 0.002843 0.010481 0.027160 4207.500 2724.21 1.001810
guess[1] 0.085580 0.000287 0.019964 0.020350 0.049502 0.084463 0.127032 4795.940 2804.12 1.000980
slip[1] 0.052194 0.000189 0.012806 0.012620 0.029235 0.051793 0.079707 4452.090 2495.93 1.000910
kc_1 lp__ -340.297000 0.060539 1.723020 1.493720 -344.523000 -339.917000 -338.091000 826.701 1095.63 1.007990
logit_pi_know_group[1] -1.500390 0.015286 0.647648 0.582706 -2.934170 -1.437650 -0.369026 2003.180 1225.40 1.001450
logit_learn_group[1] -2.176250 0.005037 0.244840 0.249966 -2.685710 -2.163220 -1.723290 2433.350 1341.87 1.000300
logit_forget_group[1] -4.456850 0.015707 0.623420 0.561498 -5.868850 -4.394220 -3.438270 2194.710 1070.80 1.007950
logit_guess_group[1] -0.002974 0.005936 0.280825 0.280056 -0.524340 -0.003166 0.547373 2301.440 1214.81 1.002410
logit_slip_group[1] -2.132630 0.006200 0.271768 0.277372 -2.671390 -2.123150 -1.640220 1990.850 1311.78 1.004500
pi_know[1] 0.200326 0.002015 0.092390 0.090590 0.050490 0.191910 0.408776 2003.160 1225.40 1.001440
learn[1] 0.104065 0.000465 0.022534 0.022647 0.063822 0.103102 0.151448 2433.320 1341.87 1.000640
forget[1] 0.013473 0.000146 0.007351 0.006525 0.002818 0.012198 0.031121 2194.710 1070.80 1.004780
guess[1] 0.249612 0.000724 0.034398 0.034903 0.185919 0.249605 0.316763 2301.420 1214.81 1.002410
slip[1] 0.054342 0.000287 0.012875 0.013260 0.032341 0.053433 0.081218 1990.840 1311.78 1.004980
[13]:
bkt_params
[13]:
{'prior': [0.4, 0.1],
 'learn': [0.04, 0.08],
 'forget': [0.01, 0.005],
 'guess': [0.1, 0.3],
 'slip': [0.05, 0.05]}

Predictions#

StanBKT offers two methods to generate predictions for the hidden state probabilities (i.e. the probability that a student knows a skill) and correctness:

  • Point Estimates: Uses a Bayesian point estimate (mean, median, or mode) of the parameter posteriors. Implemented in Python with Numba JIT compilation for fast inference. Useful for quick evaluation and debugging.

  • Posterior: Uses the full posterior to generate posterior predictive distributions via Stan’s generated quantities block. This propagates parameter uncertainty through to the predictions.

Additionally, there are two types of predictions available:

  1. Unsmoothed (online / forward): At each time step \(t\), the mastery estimate \(P(\text{know}_t \mid \text{obs}_1, \ldots, \text{obs}_{t-1})\) conditions only on previous observations. This is the standard BKT forward pass and reflects what would be known in a live tutoring system.

  2. Smoothed (offline / forward-backward): At each time step \(t\), the mastery estimate \(P(\text{know}_t \mid \text{obs}_1, \ldots, \text{obs}_T)\) conditions on all observations (past and future). This uses the HMM forward-backward algorithm and is more accurate in retrospect, but requires the full sequence to be observed first.

Both prediction methods return a long-format pd.DataFrame with columns kc_id, student_id, problem_id, pKnow, pCorrectness, and correct.

Bayesian point-estimate based (numba implementation)#

Unsmoothed (Online) Predictions#

model.predict(...) runs the standard BKT forward pass. For each time step, the mastery estimate only uses responses from prior time steps.

[14]:
# Unsmoothed (online) point-estimate predictions
# pKnow at time t is conditioned on observations 1 ... t-1 (forward pass only)
predictions = model.predict(data_df, column_mapping=col_mapping)
predictions.head(23)
[14]:
kc_id student_id problem_id pKnow pCorrectness correct
0 kc_1 stu_0 prob_0 0.200326 0.389048 0
1 kc_1 stu_0 prob_1 0.119789 0.332990 0
2 kc_1 stu_0 prob_2 0.112677 0.328040 0
3 kc_1 stu_0 prob_14 0.112106 0.327643 1
4 kc_1 stu_0 prob_17 0.389600 0.520791 0
5 kc_1 stu_0 prob_18 0.143052 0.349183 0
6 kc_1 stu_0 prob_20 0.114606 0.329383 1
7 kc_1 stu_0 prob_24 0.394424 0.524149 0
8 kc_1 stu_0 prob_25 0.143813 0.349713 1
9 kc_1 stu_0 prob_26 0.447242 0.560913 1
10 kc_1 stu_0 prob_27 0.769457 0.785190 1
11 kc_1 stu_0 prob_28 0.921852 0.891264 1
12 kc_1 stu_0 prob_29 0.967213 0.922837 1
13 kc_1 stu_0 prob_31 0.978701 0.930833 1
14 kc_1 stu_0 prob_33 0.981487 0.932772 1
15 kc_1 stu_0 prob_34 0.982155 0.933238 1
16 kc_1 stu_0 prob_36 0.982315 0.933349 1
17 kc_1 stu_0 prob_38 0.982353 0.933375 1
18 kc_1 stu_0 prob_39 0.982362 0.933382 1
19 kc_1 stu_0 prob_43 0.982364 0.933383 1
20 kc_1 stu_0 prob_47 0.982365 0.933384 1
21 kc_1 stu_0 prob_48 0.982365 0.933384 1
22 kc_1 stu_0 prob_51 0.982365 0.933384 1

Smoothed (Offline) Predictions#

model.predict_smoothed_states(...) runs the forward-backward algorithm. For each time step, the mastery estimate uses all observations in the sequence, giving a more accurate retrospective view of mastery.

[15]:
# Smoothed (offline) point-estimate predictions
# pKnow at time t is conditioned on all observations 1 ... T (forward-backward pass)
smoothed_predictions = model.predict_smoothed(data_df, column_mapping=col_mapping)
smoothed_predictions.head(n=23)
[15]:
kc_id student_id problem_id pKnow pCorrectness correct
0 kc_1 stu_0 prob_0 0.000309 0.249827 0
1 kc_1 stu_0 prob_1 0.000250 0.249786 0
2 kc_1 stu_0 prob_2 0.001194 0.250442 0
3 kc_1 stu_0 prob_14 0.013183 0.258787 1
4 kc_1 stu_0 prob_17 0.007438 0.254789 0
5 kc_1 stu_0 prob_18 0.022183 0.265052 0
6 kc_1 stu_0 prob_20 0.214357 0.398814 1
7 kc_1 stu_0 prob_24 0.253830 0.426289 0
8 kc_1 stu_0 prob_25 0.821093 0.821131 1
9 kc_1 stu_0 prob_26 0.956724 0.915537 1
10 kc_1 stu_0 prob_27 0.989153 0.938109 1
11 kc_1 stu_0 prob_28 0.996907 0.943506 1
12 kc_1 stu_0 prob_29 0.998761 0.944796 1
13 kc_1 stu_0 prob_31 0.999204 0.945105 1
14 kc_1 stu_0 prob_33 0.999310 0.945178 1
15 kc_1 stu_0 prob_34 0.999335 0.945196 1
16 kc_1 stu_0 prob_36 0.999341 0.945200 1
17 kc_1 stu_0 prob_38 0.999343 0.945201 1
18 kc_1 stu_0 prob_39 0.999343 0.945201 1
19 kc_1 stu_0 prob_43 0.999340 0.945199 1
20 kc_1 stu_0 prob_47 0.999330 0.945192 1
21 kc_1 stu_0 prob_48 0.999288 0.945163 1
22 kc_1 stu_0 prob_51 0.999111 0.945040 1

Posterior Predictions#

The following functions with _posterior_ in the function name runs the generate_quantities block in the stan model to generate mastery and correctness predictions.

Notes:

  • Using functions with _posterior_stan suffix will return a dictionary mapping each Kc to the associated raw Stan fit object. This is beneficial to advanced users who want to run additional analysis or directly call cmdstanpy methods.

  • Using functions with _posterior_draws suffix will return a dictionary mapping each Kc to a pandas DataFrame containing the draws from the posterior distribution.

Unsmoothed (Online) Predictions#

[16]:
pred_post_draws = model.predict_posterior_draws(data_df, column_mapping=col_mapping)
11:15:03 - cmdstanpy - INFO - compiling stan file /home/sppradhan/Desktop/Research/StanBKT/src/stanbkt/stan_code/BKT/hidden_states.stan to exe file /home/sppradhan/Desktop/Research/StanBKT/src/stanbkt/stan_code/BKT/hidden_states
11:15:16 - cmdstanpy - INFO - compiled model executable: /home/sppradhan/Desktop/Research/StanBKT/src/stanbkt/stan_code/BKT/hidden_states
11:15:16 - cmdstanpy - INFO - Chain [1] start processing
11:15:16 - cmdstanpy - INFO - Chain [2] start processing
11:15:16 - cmdstanpy - INFO - Chain [3] start processing
11:15:16 - cmdstanpy - INFO - Chain [4] start processing
11:15:16 - cmdstanpy - INFO - Chain [3] done processing
11:15:16 - cmdstanpy - INFO - Chain [2] done processing
11:15:16 - cmdstanpy - INFO - Chain [4] done processing
11:15:16 - cmdstanpy - INFO - Chain [1] done processing
11:15:16 - cmdstanpy - INFO - Chain [1] start processing
11:15:16 - cmdstanpy - INFO - Chain [2] start processing
11:15:16 - cmdstanpy - INFO - Chain [3] start processing
11:15:16 - cmdstanpy - INFO - Chain [4] start processing
11:15:16 - cmdstanpy - INFO - Chain [2] done processing
11:15:16 - cmdstanpy - INFO - Chain [3] done processing
11:15:16 - cmdstanpy - INFO - Chain [4] done processing
11:15:16 - cmdstanpy - INFO - Chain [1] done processing
11:15:16 - cmdstanpy - WARNING - Sample doesn't contain draws from warmup iterations, rerun sampler with "save_warmup=True".
11:15:17 - cmdstanpy - WARNING - Sample doesn't contain draws from warmup iterations, rerun sampler with "save_warmup=True".

The draws are a dictionary mapping KC –> Posterior Draws

[17]:
pred_post_draws
[17]:
{'kc_1':          chain__  iter__  draw__ kc_id student_id problem_id  correct  _order  \
 0            1.0     1.0     1.0  kc_1      stu_0     prob_0        0       0
 1            1.0     1.0     1.0  kc_1      stu_0     prob_1        0       1
 2            1.0     1.0     1.0  kc_1      stu_0     prob_2        0       2
 3            1.0     1.0     1.0  kc_1      stu_0    prob_14        1       3
 4            1.0     1.0     1.0  kc_1      stu_0    prob_17        0       4
 ...          ...     ...     ...   ...        ...        ...      ...     ...
 1531995      4.0   500.0  2000.0  kc_1     stu_29    prob_45        0      18
 1531996      4.0   500.0  2000.0  kc_1     stu_29    prob_47        0      19
 1531997      4.0   500.0  2000.0  kc_1     stu_29    prob_48        0      20
 1531998      4.0   500.0  2000.0  kc_1     stu_29    prob_51        1      21
 1531999      4.0   500.0  2000.0  kc_1     stu_29    prob_55        0      22

             pKnow  pCorrectness
 0        0.072738      0.327083
 1        0.092467      0.340404
 2        0.093838      0.341329
 3        0.093936      0.341395
 4        0.326794      0.498607
 ...           ...           ...
 1531995  0.343477      0.512221
 1531996  0.135087      0.377133
 1531997  0.107874      0.359493
 1531998  0.105167      0.357738
 1531999  0.343322      0.512120

 [1532000 rows x 10 columns],
 'kc_0':          chain__  iter__  draw__ kc_id student_id problem_id  correct  _order  \
 0            1.0     1.0     1.0  kc_0      stu_0     prob_4        0       0
 1            1.0     1.0     1.0  kc_0      stu_0     prob_6        0       1
 2            1.0     1.0     1.0  kc_0      stu_0     prob_7        1       2
 3            1.0     1.0     1.0  kc_0      stu_0     prob_8        1       3
 4            1.0     1.0     1.0  kc_0      stu_0     prob_9        1       4
 ...          ...     ...     ...   ...        ...        ...      ...     ...
 2695995      4.0  1000.0  4000.0  kc_0     stu_29    prob_53        1      17
 2695996      4.0  1000.0  4000.0  kc_0     stu_29    prob_54        1      18
 2695997      4.0  1000.0  4000.0  kc_0     stu_29    prob_56        1      19
 2695998      4.0  1000.0  4000.0  kc_0     stu_29    prob_57        1      20
 2695999      4.0  1000.0  4000.0  kc_0     stu_29    prob_59        1      21

             pKnow  pCorrectness
 0        0.461994      0.494797
 1        0.110926      0.189075
 2        0.084931      0.166437
 3        0.528236      0.552483
 4        0.919533      0.893239
 ...           ...           ...
 2695995  0.973452      0.932393
 2695996  0.973452      0.932393
 2695997  0.973452      0.932393
 2695998  0.973452      0.932393
 2695999  0.973452      0.932393

 [2696000 rows x 10 columns]}

Smoothed (Offline) Predictions#

[18]:
smoothed_post_draws = model.predict_smoothed_posterior_draws(
    data_df, column_mapping=col_mapping
)
11:15:18 - cmdstanpy - INFO - compiling stan file /home/sppradhan/Desktop/Research/StanBKT/src/stanbkt/stan_code/BKT/smoothed_hidden_states.stan to exe file /home/sppradhan/Desktop/Research/StanBKT/src/stanbkt/stan_code/BKT/smoothed_hidden_states
11:15:30 - cmdstanpy - INFO - compiled model executable: /home/sppradhan/Desktop/Research/StanBKT/src/stanbkt/stan_code/BKT/smoothed_hidden_states
11:15:30 - cmdstanpy - INFO - Chain [1] start processing
11:15:30 - cmdstanpy - INFO - Chain [2] start processing
11:15:30 - cmdstanpy - INFO - Chain [3] start processing
11:15:30 - cmdstanpy - INFO - Chain [4] start processing
11:15:31 - cmdstanpy - INFO - Chain [4] done processing
11:15:31 - cmdstanpy - INFO - Chain [1] done processing
11:15:31 - cmdstanpy - INFO - Chain [2] done processing
11:15:31 - cmdstanpy - INFO - Chain [3] done processing
11:15:31 - cmdstanpy - INFO - Chain [1] start processing
11:15:31 - cmdstanpy - INFO - Chain [2] start processing
11:15:31 - cmdstanpy - INFO - Chain [3] start processing
11:15:31 - cmdstanpy - INFO - Chain [4] start processing
11:15:31 - cmdstanpy - INFO - Chain [1] done processing
11:15:31 - cmdstanpy - INFO - Chain [3] done processing
11:15:31 - cmdstanpy - INFO - Chain [2] done processing
11:15:31 - cmdstanpy - INFO - Chain [4] done processing
11:15:31 - cmdstanpy - WARNING - Sample doesn't contain draws from warmup iterations, rerun sampler with "save_warmup=True".
11:15:32 - cmdstanpy - WARNING - Sample doesn't contain draws from warmup iterations, rerun sampler with "save_warmup=True".
[19]:
smoothed_post_draws
[19]:
{'kc_1':          chain__  iter__  draw__ kc_id student_id problem_id  correct  _order  \
 0            1.0     1.0     1.0  kc_1      stu_0     prob_0        0       0
 1            1.0     1.0     1.0  kc_1      stu_0     prob_1        0       1
 2            1.0     1.0     1.0  kc_1      stu_0     prob_2        0       2
 3            1.0     1.0     1.0  kc_1      stu_0    prob_14        1       3
 4            1.0     1.0     1.0  kc_1      stu_0    prob_17        0       4
 ...          ...     ...     ...   ...        ...        ...      ...     ...
 1531995      4.0   500.0  2000.0  kc_1     stu_29    prob_45        0      18
 1531996      4.0   500.0  2000.0  kc_1     stu_29    prob_47        0      19
 1531997      4.0   500.0  2000.0  kc_1     stu_29    prob_48        0      20
 1531998      4.0   500.0  2000.0  kc_1     stu_29    prob_51        1      21
 1531999      4.0   500.0  2000.0  kc_1     stu_29    prob_55        0      22

             pKnow  pCorrectness
 0        0.000007      0.277980
 1        0.000026      0.277992
 2        0.000263      0.278152
 3        0.003595      0.280402
 4        0.003985      0.280665
 ...           ...           ...
 1531995  0.000500      0.289888
 1531996  0.000564      0.289930
 1531997  0.003777      0.292013
 1531998  0.037448      0.313840
 1531999  0.043765      0.317935

 [1532000 rows x 10 columns],
 'kc_0':          chain__  iter__  draw__ kc_id student_id problem_id  correct  _order  \
 0            1.0     1.0     1.0  kc_0      stu_0     prob_4        0       0
 1            1.0     1.0     1.0  kc_0      stu_0     prob_6        0       1
 2            1.0     1.0     1.0  kc_0      stu_0     prob_7        1       2
 3            1.0     1.0     1.0  kc_0      stu_0     prob_8        1       3
 4            1.0     1.0     1.0  kc_0      stu_0     prob_9        1       4
 ...          ...     ...     ...   ...        ...        ...      ...     ...
 2695995      4.0  1000.0  4000.0  kc_0     stu_29    prob_53        1      17
 2695996      4.0  1000.0  4000.0  kc_0     stu_29    prob_54        1      18
 2695997      4.0  1000.0  4000.0  kc_0     stu_29    prob_56        1      19
 2695998      4.0  1000.0  4000.0  kc_0     stu_29    prob_57        1      20
 2695999      4.0  1000.0  4000.0  kc_0     stu_29    prob_59        1      21

             pKnow  pCorrectness
 0        0.016394      0.106752
 1        0.053654      0.139200
 2        0.915737      0.889933
 3        0.992430      0.956720
 4        0.999253      0.962662
 ...           ...           ...
 2695995  0.999835      0.955229
 2695996  0.999834      0.955227
 2695997  0.999816      0.955212
 2695998  0.999618      0.955041
 2695999  0.997442      0.953157

 [2696000 rows x 10 columns]}

Summarising the posterior predictions#

StanBKT conveniently provides a posterior_summary utility function to summarize the results from the posteriors prediction functions: predict_posterior_draws and predict_smoothed_posterior_draws (or the _stan versions).

We use the posterior_summary to summarize the draws from the smoothed posterior predictions and produce 90% credible intervals.

[20]:
from stanbkt.utils import posterior_summary

posterior_summary(smoothed_post_draws, col_mapping=col_mapping, quantiles=[0.05, 0.95])
[20]:
kc_id student_id problem_id correct pKnow_mean pKnow_std pKnow_median pKnow_5.00% pKnow_95.00% pCorrectness_mean pCorrectness_std pCorrectness_median pCorrectness_5.00% pCorrectness_95.00%
0 kc_1 stu_0 prob_0 0 0.000342 0.000334 0.000257 0.000050 0.000928 0.249850 0.034346 0.249795 0.194498 0.306617
1 kc_1 stu_0 prob_1 0 0.000287 0.000213 0.000235 0.000071 0.000651 0.249811 0.034358 0.249868 0.194459 0.306601
2 kc_1 stu_0 prob_2 0 0.001378 0.000912 0.001175 0.000385 0.003085 0.250574 0.034150 0.250675 0.196358 0.306993
3 kc_1 stu_0 prob_14 1 0.014745 0.007668 0.013107 0.005507 0.028959 0.259959 0.031720 0.259407 0.209147 0.312221
4 kc_1 stu_0 prob_17 0 0.008546 0.005360 0.007257 0.002381 0.018776 0.255562 0.032998 0.255502 0.203013 0.310519
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
1435 kc_0 stu_9 prob_53 0 0.911474 0.088229 0.939377 0.744974 0.987227 0.871469 0.067811 0.892823 0.742354 0.928015
1436 kc_0 stu_9 prob_54 1 0.991631 0.010277 0.994869 0.974127 0.999056 0.940629 0.010616 0.941636 0.922044 0.956053
1437 kc_0 stu_9 prob_56 1 0.998998 0.001529 0.999453 0.996726 0.999905 0.946951 0.012283 0.947604 0.925489 0.965458
1438 kc_0 stu_9 prob_57 1 0.999624 0.000511 0.999765 0.998905 0.999942 0.947486 0.012666 0.947970 0.925592 0.966811
1439 kc_0 stu_9 prob_59 1 0.998712 0.000900 0.998924 0.997045 0.999646 0.946702 0.012621 0.947218 0.924925 0.965995

1440 rows × 14 columns

Visualizing Posterior#

StanBKT provides a plotting function to visualize the posterior distribution for correctness. This plots the ground truth proportion correct for problems in a KC along with: either the estimated probability of correctness, or correctness predictions using the posterior (sampled from a Bernoulli distribution).

[21]:
from stanbkt.plot import plot_posterior_correctness
[ ]:
# probability of correctness (with credible intervals)
plot_posterior_correctness(
    posterior_pred_kc=pred_post_draws["kc_1"],
    data=data_df,
    kc="kc_1",
    type="probs",
    trajectory=True,
    frac=1,  # all problems
)
<Axes: title={'center': 'Posterior Correctness — kc_1'}, xlabel='Problem ID', ylabel='Prob/Prop of Correctness'>
../_images/examples_02_simple_example_40_1.png
[ ]:
# correctness predictions (with predictive intervals)
plot_posterior_correctness(
    posterior_pred_kc=pred_post_draws["kc_1"],
    data=data_df,
    posterior_pred_kc=pred_post_draws["kc_1"],
    kc="kc_1",
    type="preds",
    trajectory=True,
    frac=0.5,
)
<Axes: title={'center': 'Posterior Correctness — kc_1'}, xlabel='Problem ID', ylabel='Proportion Correct'>
../_images/examples_02_simple_example_41_1.png