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_dat = 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)
)
astrom_like = PlanetRelAstromLikelihood(astrom_dat, name="simulated astrom")

planet_b = Planet(
    name="b",
    basis=Visual{KepOrbit},
    likelihoods=[astrom_like],
    variables=@variables 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(θ,50420; system.M, a, e, i, ω, Ω)  # reference epoch for θ. Choose an MJD date near your data.
    end
)

sys = System(
    name="Tutoria",
    companions=[planet_b],
    likelihoods=[],
    variables=@variables begin
        M ~ truncated(Normal(1.2, 0.1), lower=0.1)
        plx ~ truncated(Normal(50.0, 0.02), lower=0.1)
    end
)

model = Octofitter.LogDensityModel(sys)

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.77 seconds
Compute duration  = 3.77 seconds
parameters        = M, plx, b_a, b_e, b_i, b_ωx, b_ωy, b_Ωx, b_Ωy, b_θx, b_θy, 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      r ⋯
      Symbol      Float64     Float64    Float64     Float64    Float64   Floa ⋯

           M       1.1821      0.0949     0.0034    768.1107   576.2198    1.0 ⋯
         plx      49.9996      0.0196     0.0005   1474.4368   654.8202    1.0 ⋯
         b_a      11.6641      2.1484     0.1417    212.5453   425.2264    1.0 ⋯
         b_e       0.1405      0.1012     0.0053    368.9211   392.1304    1.0 ⋯
         b_i       0.6335      0.1531     0.0099    258.1252   248.3632    1.0 ⋯
        b_ωx       0.0038      0.7149     0.0534    208.0390   699.7637    1.0 ⋯
        b_ωy      -0.0769      0.7054     0.0517    201.9895   543.4901    1.0 ⋯
        b_Ωx      -0.2095      0.7151     0.0928     74.9446   298.4757    1.0 ⋯
        b_Ωy      -0.1845      0.6564     0.0896     68.2649   359.0669    1.0 ⋯
        b_θx       0.0747      0.0105     0.0004    878.6689   630.9839    0.9 ⋯
        b_θy      -1.0021      0.1009     0.0035    858.9123   551.0444    1.0 ⋯
         b_ω      -0.2666      1.7822     0.1134    300.5113   645.1110    1.0 ⋯
         b_Ω      -0.9827      1.7900     0.2160    112.1317   218.4142    1.0 ⋯
         b_θ      -1.4964      0.0073     0.0002    934.5854   641.3347    1.0 ⋯
        b_tp   42792.9699   5011.3533   316.6178    237.4203   470.0924    1.0 ⋯
                                                               2 columns omitted

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

           M       1.0016       1.1199       1.1828       1.2411       1.3750
         plx      49.9603      49.9862      49.9992      50.0137      50.0382
         b_a       8.3480      10.0972      11.2598      12.9488      16.5075
         b_e       0.0070       0.0635       0.1191       0.2032       0.3757
         b_i       0.2907       0.5416       0.6458       0.7415       0.8884
        b_ωx      -1.0691      -0.6843       0.0009       0.7202       1.0481
        b_ωy      -1.0505      -0.7658      -0.1363       0.5886       1.0740
        b_Ωx      -1.0854      -0.8424      -0.4205       0.4589       1.0509
        b_Ωy      -1.0670      -0.7483      -0.3475       0.3709       1.0263
        b_θx       0.0554       0.0675       0.0742       0.0813       0.0971
        b_θy      -1.2141      -1.0644      -0.9986      -0.9341      -0.8162
         b_ω      -2.9751      -1.9530      -0.3123       1.1381       2.9544
         b_Ω      -3.0144      -2.5143      -1.8051       0.5127       2.9413
         b_θ      -1.5104      -1.5012      -1.4964      -1.4916      -1.4824
        b_tp   30799.4910   40091.7572   43748.8994   46457.6653   49776.6869

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(9.96, 0.146, 0.538, -2.14, 3.64, 44700.0, 1.2), 49.98615809188959, 4.1264384805873577e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.5, 0.0798, 0.538, -2.24, 4.39, 46500.0, 1.27), 49.973050541965925, 4.127520813921118e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.59, 0.197, 0.589, 3.06, 4.54, 45600.0, 1.23), 50.02191526973248, 4.1234887775659426e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.8, 0.0122, 0.783, 2.74, 3.34, 37300.0, 1.3), 49.99349107718122, 4.1258332195417103e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.6, 0.0091, 0.704, -1.67, 3.98, 44700.0, 1.06), 49.98180890535762, 4.1267975442358702e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.82, 0.186, 0.585, -2.11, 3.51, 44000.0, 1.0), 50.06796943692027, 4.1196958567885533e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(15.5, 0.0642, 0.861, -0.925, 0.264, 33200.0, 1.32), 50.0234326064724, 4.123363701762608e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.5, 0.338, 0.638, 0.416, 4.86, 32500.0, 1.16), 50.00721509350455, 4.1247009228851893e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.6, 0.179, 0.437, 1.75, 5.13, 43500.0, 1.24), 49.99183676973666, 4.1259697497644653e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.6, 0.0591, 0.726, 1.9, 3.51, 34700.0, 1.17), 49.99760817979972, 4.1254934737144583e6)
 ⋮
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.93, 0.0853, 0.625, -2.01, 4.14, 45800.0, 1.13), 49.984473884654996, 4.1265775193128265e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.9, 0.0381, 0.704, -1.2, 0.408, 36300.0, 1.18), 49.985418109039045, 4.126499568277028e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.3, 0.089, 0.721, -0.0826, 3.93, 48200.0, 1.08), 49.98405411946963, 4.126612174236438e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.5, 0.0107, 0.542, -1.94, 4.09, 45600.0, 1.23), 49.99493424182435, 4.1257141223428403e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.2, 0.0253, 0.608, -0.599, 3.87, 47200.0, 1.11), 50.0084925686493, 4.1245955567236054e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.2, 0.228, 0.382, -2.46, 2.99, 42300.0, 1.19), 49.98452898043077, 4.126572970765619e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.1, 0.224, 0.532, -2.76, 2.18, 36700.0, 1.03), 50.00751323261129, 4.124676331887373e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.0, 0.149, 0.424, 0.0875, 6.27, 40100.0, 1.04), 50.022812922367024, 4.1234147821156983e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.23, 0.179, 0.478, 0.56, 0.771, 44900.0, 1.18), 50.00512288451952, 4.1248734999299715e6)

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.