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, 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)
    plx ~ truncated(Normal(50.0, 0.02), lower=0)
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.88 seconds
Compute duration  = 3.88 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      r ⋯
      Symbol      Float64     Float64    Float64     Float64    Float64   Floa ⋯

           M       1.1863      0.0978     0.0063    237.3985   446.4125    1.0 ⋯
         plx      49.9998      0.0183     0.0006   1073.2701   614.9544    1.0 ⋯
         b_a      11.9373      2.2558     0.1411    244.7130   434.4190    1.0 ⋯
         b_e       0.1507      0.1088     0.0059    346.6565   464.3563    1.0 ⋯
         b_i       0.6484      0.1595     0.0136    182.2556    48.3513    1.0 ⋯
        b_ωy      -0.1467      0.7151     0.0606    152.3857   553.8900    1.0 ⋯
        b_ωx       0.0439      0.6995     0.0517    191.5746   627.1462    1.0 ⋯
        b_Ωy       0.1647      0.7489     0.1014     72.7548   351.9880    1.0 ⋯
        b_Ωx       0.2064      0.6242     0.0790     75.2757   277.2631    1.0 ⋯
        b_θy       0.0745      0.0104     0.0004    594.4027   664.9113    1.0 ⋯
        b_θx      -1.0018      0.1022     0.0041    599.0980   646.8931    1.0 ⋯
         b_ω      -0.1394      1.9782     0.1197    391.5292   656.9181    1.0 ⋯
         b_Ω      -0.0124      1.6764     0.1724    212.5306   391.0570    1.0 ⋯
         b_θ      -1.4966      0.0073     0.0002    946.5030   445.8015    1.0 ⋯
        b_tp   43169.6849   4963.2786   297.2918    282.7560   478.1124    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.0017       1.1205       1.1865       1.2460       1.3890
         plx      49.9657      49.9873      49.9995      50.0126      50.0370
         b_a       8.5265      10.3040      11.5296      13.1584      17.4011
         b_e       0.0083       0.0596       0.1272       0.2237       0.3900
         b_i       0.2524       0.5636       0.6605       0.7610       0.9090
        b_ωy      -1.1099      -0.8182      -0.3119       0.5708       1.0193
        b_ωx      -1.0261      -0.6289       0.0438       0.7191       1.0612
        b_Ωy      -1.0562      -0.6371       0.3930       0.8619       1.0715
        b_Ωx      -0.9877      -0.3066       0.3619       0.7359       1.0482
        b_θy       0.0561       0.0671       0.0736       0.0814       0.0958
        b_θx      -1.2140      -1.0694      -0.9994      -0.9297      -0.8196
         b_ω      -3.0535      -2.1561       0.1623       1.5202       3.0482
         b_Ω      -2.9961      -1.6903       0.4344       1.0967       2.9229
         b_θ      -1.5118      -1.5014      -1.4967      -1.4917      -1.4821
        b_tp   31296.7229   40292.9830   44175.3197   46940.4300   49827.1601

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(chain, :b, ii)
50-element Vector{Visual{KepOrbit{Float64}, Float64}}:
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.7, 0.119, 0.75, -3.11, 0.49, 47800.0, 1.28), 49.99961318468417, 4.1253319148313506e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.2, 0.00517, 0.613, 1.44, 1.21, 46700.0, 1.25), 50.006450964884905, 4.124767825352005e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.5, 0.0218, 0.789, 2.14, 0.356, 44100.0, 1.15), 50.020366220760415, 4.1236203487529033e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.2, 0.278, 0.571, 1.33, 5.23, 41800.0, 1.2), 49.98660625505593, 4.1264053604186657e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.8, 0.401, 0.824, -2.65, 2.96, 35300.0, 1.31), 49.99422346501079, 4.125776653863974e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.95, 0.0674, 0.384, 1.96, 4.4, 41000.0, 0.896), 50.0165692328313, 4.1239333917490267e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.65, 0.269, 0.444, 1.39, 6.2, 45000.0, 1.01), 49.978123956661754, 4.127105694860846e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.6, 0.00953, 0.705, -1.53, 0.479, 36100.0, 1.14), 50.00977092362149, 4.1244939976834273e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(17.5, 0.13, 0.925, -2.38, 0.387, 48700.0, 1.35), 49.99321183838555, 4.1258601401085937e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.28, 0.0847, 0.445, 2.29, 4.86, 44100.0, 1.12), 49.96741956577682, 4.127989834025228e6)
 ⋮
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.1, 0.103, 0.805, 2.04, 0.746, 46200.0, 1.32), 49.97860449106273, 4.127066013555554e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.2, 0.172, 0.402, -2.57, 3.13, 41700.0, 0.979), 49.99858141682606, 4.1254170449441085e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.4, 0.051, 0.745, 3.03, 3.7, 39800.0, 1.19), 50.01856340882588, 4.1237689758119704e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.0, 0.171, 0.734, 1.05, 0.44, 43800.0, 1.24), 50.00071388512431, 4.1252411010348755e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.6, 0.14, 0.627, 2.87, 1.2, 49000.0, 1.06), 50.004343280850705, 4.124941684395438e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.8, 0.0729, 0.66, 0.72, 0.292, 41900.0, 1.19), 49.999263683036055, 4.125360751462074e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.3, 0.24, 0.691, -1.66, 3.25, 44600.0, 1.16), 50.020186470918155, 4.1236351671724156e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(16.5, 0.0644, 0.907, 1.58, 3.48, 28200.0, 1.15), 50.00467916186937, 4.1249139771960694e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.37, 0.198, 0.489, -1.31, 1.88, 43600.0, 1.23), 49.998395498995784, 4.1254323852080978e6)

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.