Posterior Predictive Checks

A posterior predictive check compares our true data with simulated data drawn from the posterior. This allows us to evaluate if the model is able to reproduce our observations appropriately. Samples drawn from the posterior predictive distribution should match the locations of the original data.

To demonstrate, we will fit a model to relative astrometry data:

using Octofitter
using Distributions

astrom_like = PlanetRelAstromLikelihood(Table(;
    epoch= [50000,50120,50240,50360,50480,50600,50720,50840,],
    ra = [-505.764,-502.57,-498.209,-492.678,-485.977,-478.11,-469.08,-458.896,],
    dec = [-66.9298,-37.4722,-7.92755,21.6356, 51.1472, 80.5359, 109.729, 138.651,],
    σ_ra = fill(10.0, 8),
    σ_dec = fill(10.0, 8),
    cor = fill(0.0, 8)
))
@planet b Visual{KepOrbit} begin
    a ~ truncated(Normal(10, 4), lower=0.1, upper=100)
    e ~ Uniform(0.0, 0.5)
    i ~ Sine()
    ω ~ UniformCircular()
    Ω ~ UniformCircular()
    θ ~ UniformCircular()
    tp = θ_at_epoch_to_tperi(system,b,50420)  # reference epoch for θ. Choose an MJD date near your data.
end astrom_like
@system Tutoria begin
    M ~ truncated(Normal(1.2, 0.1), lower=0.1)
    plx ~ truncated(Normal(50.0, 0.02), lower=0.1)
end b
model = Octofitter.LogDensityModel(Tutoria)

using Random
Random.seed!(0)
chain = octofit(model)
Chains MCMC chain (1000×28×1 Array{Float64, 3}):

Iterations        = 1:1:1000
Number of chains  = 1
Samples per chain = 1000
Wall duration     = 3.15 seconds
Compute duration  = 3.15 seconds
parameters        = M, plx, b_a, b_e, b_i, b_ωy, b_ωx, b_Ωy, b_Ωx, b_θy, b_θx, b_ω, b_Ω, b_θ, b_tp
internals         = n_steps, is_accept, acceptance_rate, hamiltonian_energy, hamiltonian_energy_error, max_hamiltonian_energy_error, tree_depth, numerical_error, step_size, nom_step_size, is_adapt, loglike, logpost, tree_depth, numerical_error

Summary Statistics
  parameters         mean         std       mcse   ess_bulk   ess_tail      rh ⋯
      Symbol      Float64     Float64    Float64    Float64    Float64   Float ⋯

           M       1.1897      0.0956     0.0043   505.0070   387.9868    1.00 ⋯
         plx      49.9994      0.0193     0.0007   867.5579   526.0011    1.00 ⋯
         b_a      11.6771      2.3103     0.1737   171.0444   339.7757    0.99 ⋯
         b_e       0.1589      0.1088     0.0068   270.6389   249.3773    1.00 ⋯
         b_i       0.6387      0.1547     0.0109   207.4897   225.0259    0.99 ⋯
        b_ωy      -0.0873      0.7247     0.0550   194.8158   561.0269    1.00 ⋯
        b_ωx      -0.0659      0.6982     0.0550   173.9539   674.3650    1.01 ⋯
        b_Ωy      -0.2036      0.7266     0.0980    63.9441   489.7963    1.00 ⋯
        b_Ωx      -0.1518      0.6528     0.0917    59.3963   349.1122    1.00 ⋯
        b_θy       0.0755      0.0103     0.0004   688.2825   660.9260    1.00 ⋯
        b_θx      -1.0112      0.0994     0.0040   609.2988   579.4884    1.00 ⋯
         b_ω      -0.4131      1.8710     0.1290   287.2006   632.9315    1.00 ⋯
         b_Ω      -0.8571      1.8605     0.2106   107.6718   363.7688    1.00 ⋯
         b_θ      -1.4962      0.0071     0.0003   734.6148   601.8661    1.00 ⋯
        b_tp   43199.7104   4729.4213   319.0570   274.8580   267.4786    1.00 ⋯
                                                               2 columns omitted

Quantiles
  parameters         2.5%        25.0%        50.0%        75.0%        97.5%
      Symbol      Float64      Float64      Float64      Float64      Float64

           M       1.0140       1.1224       1.1910       1.2545       1.3773
         plx      49.9598      49.9866      50.0003      50.0119      50.0364
         b_a       8.2603      10.0363      11.2140      12.9092      16.8397
         b_e       0.0055       0.0663       0.1441       0.2339       0.3820
         b_i       0.3159       0.5315       0.6501       0.7512       0.9109
        b_ωy      -1.0776      -0.7603      -0.1868       0.6182       1.0760
        b_ωx      -1.0672      -0.7117      -0.1574       0.5983       1.0721
        b_Ωy      -1.0887      -0.8646      -0.4211       0.5275       1.0310
        b_Ωx      -1.0749      -0.7366      -0.2749       0.4433       1.0199
        b_θy       0.0572       0.0689       0.0745       0.0816       0.0980
        b_θx      -1.2176      -1.0799      -1.0086      -0.9404      -0.8274
         b_ω      -3.0123      -2.1945      -0.4584       1.1681       2.9963
         b_Ω      -3.0368      -2.5116      -1.6539       0.6258       2.9628
         b_θ      -1.5097      -1.5010      -1.4964      -1.4913      -1.4819
        b_tp   32059.1957   40732.8858   44041.9292   46532.8566   50091.0211

We now have our posterior as approximated by the MCMC chain. Convert these posterior samples into orbit objects:

# Instead of creating orbit objects for all rows in the chain, just pick
# every twentieth row.
ii = 1:20:1000
orbits = Octofitter.construct_elements(model, chain, :b, ii)
50-element Vector{Visual{KepOrbit{Float64}, Float64}}:
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.4, 0.0199, 0.567, 2.34, 0.234, 45900.0, 1.17), 49.97798578053057, 4.1271171052346136e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.0, 0.0816, 0.782, -2.84, 0.439, 48000.0, 1.26), 50.00301809793625, 4.125051003841568e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.8, 0.0536, 0.69, -2.32, 3.64, 43000.0, 1.34), 49.98474241840482, 4.1265592262820466e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.0, 0.0436, 0.594, -1.35, 3.77, 45700.0, 1.15), 49.98729988709292, 4.1263481017357195e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.71, 0.262, 0.332, -1.99, 3.01, 44800.0, 1.2), 49.969071724644664, 4.1278533476992818e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(15.9, 0.378, 0.806, -2.96, 3.06, 32800.0, 1.27), 49.98681329300777, 4.126388269460911e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.3, 0.106, 0.613, -1.41, 4.08, 47000.0, 1.29), 50.00930996301635, 4.124532015189577e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.1, 0.0901, 0.663, 2.09, 0.779, 46700.0, 1.14), 50.02775408140705, 4.123011392123616e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.5, 0.124, 0.744, 2.21, 3.46, 33500.0, 1.15), 50.009386933112886, 4.124525667068817e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.6, 0.218, 0.362, -2.24, 3.67, 45300.0, 1.01), 50.01123584197855, 4.1243731838929043e6)
 ⋮
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.97, 0.212, 0.478, 0.927, 6.07, 43400.0, 1.31), 49.99467576390132, 4.1257393282052996e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.9, 0.0747, 0.611, -2.37, 0.794, 50100.0, 1.14), 49.99741063883237, 4.1255136488967794e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.0, 0.282, 0.765, 0.245, 6.26, 35800.0, 1.11), 50.00230620613709, 4.125109732932355e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.8, 0.0806, 0.527, 0.952, 4.48, 38700.0, 1.03), 50.009523784683, 4.1245143802624093e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.3, 0.309, 0.275, 1.33, 6.05, 44900.0, 1.03), 50.009347786747256, 4.124528895669008e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.32, 0.164, 0.473, 2.01, 4.95, 44100.0, 1.3), 49.95815720990979, 4.12875517272052e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.2, 0.0583, 0.709, 1.49, 4.63, 42000.0, 1.33), 49.99353017111963, 4.1258338687823974e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.9, 0.15, 0.699, -0.316, 4.22, 48700.0, 1.13), 49.99200983421631, 4.1259593419831838e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.0, 0.144, 0.614, -2.51, 3.41, 42300.0, 1.16), 49.97458554932959, 4.1273979110121313e6)

Calculate and plot the location the planet would be at each observation epoch:

using CairoMakie

epochs = astrom_like.table.epoch' # transpose

x = raoff.(orbits, epochs)[:]
y = decoff.(orbits, epochs)[:]

fig = Figure()
ax = Axis(
    fig[1,1], xlabel="ra offset [mas]", ylabel="dec offset [mas]",
    xreversed=true,
    aspect=1
)
for orbit in orbits
    Makie.lines!(ax, orbit, color=:lightgrey)
end

Makie.scatter!(
    ax,
    x, y,
    markersize=3,
)
fig

Makie.scatter!(ax, astrom_like.table.ra, astrom_like.table.dec,color=:black, label="observed")
Makie.scatter!(ax, [0],[0], marker='⋆', color=:black, markersize=20,label="")
Makie.xlims!(400,-700)
Makie.ylims!(-200,200)
fig
Example block output

Looks like a great match to the data! Notice how the uncertainty around the middle point is lower than the ends. That's because the orbit's posterior location at that epoch is also constrained by the surrounding data points. We can know the location of the planet in hindsight better than we could measure it!

You can follow this same procedure for any kind of data modelled with Octofitter.