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     = 2.95 seconds
Compute duration  = 2.95 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.1837      0.0960     0.0044   477.8678   461.3546    1.01 ⋯
         plx      50.0004      0.0187     0.0006   924.5617   430.3869    1.00 ⋯
         b_a      11.5898      2.1582     0.1855   134.7865   225.6521    1.00 ⋯
         b_e       0.1463      0.1012     0.0058   312.4982   503.7352    1.00 ⋯
         b_i       0.6158      0.1801     0.0144   171.9821   149.6895    1.00 ⋯
        b_ωy       0.0995      0.7484     0.0581   195.0416   503.5938    1.00 ⋯
        b_ωx       0.1372      0.6560     0.0613   132.7181   533.4917    1.01 ⋯
        b_Ωy       0.2132      0.7036     0.1611    24.1848   278.8166    1.05 ⋯
        b_Ωx       0.1017      0.6832     0.1506    27.5151   327.9320    1.03 ⋯
        b_θy       0.0742      0.0108     0.0005   569.1807   496.1622    1.00 ⋯
        b_θx      -0.9948      0.1011     0.0042   604.6958   506.1264    1.00 ⋯
         b_ω       0.1310      1.7364     0.1060   321.7505   314.3554    1.01 ⋯
         b_Ω      -0.2091      1.5628     0.3304    34.5872   236.9346    1.03 ⋯
         b_θ      -1.4964      0.0072     0.0003   707.2922   716.3752    1.00 ⋯
        b_tp   43837.7044   4559.9854   266.1271   281.9424   342.1591    1.00 ⋯
                                                               2 columns omitted

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

           M       0.9941       1.1216       1.1862       1.2482       1.3725
         plx      49.9651      49.9880      50.0007      50.0129      50.0359
         b_a       8.1061      10.0418      11.2053      12.9746      16.6591
         b_e       0.0081       0.0598       0.1373       0.2099       0.3730
         b_i       0.1701       0.5270       0.6406       0.7424       0.8989
        b_ωy      -1.0521      -0.6918       0.2699       0.8137       1.0937
        b_ωx      -1.0107      -0.4372       0.2420       0.7249       1.0490
        b_Ωy      -1.0115      -0.4841       0.4232       0.8405       1.0866
        b_Ωx      -1.0389      -0.5803       0.2999       0.6776       1.0631
        b_θy       0.0553       0.0665       0.0736       0.0808       0.0968
        b_θx      -1.2024      -1.0622      -0.9865      -0.9218      -0.8218
         b_ω      -3.0077      -1.2194       0.4030       1.3298       3.0063
         b_Ω      -2.8592      -1.8368       0.3519       0.8896       2.6692
         b_θ      -1.5105      -1.5012      -1.4967      -1.4912      -1.4825
        b_tp   33150.8580   41126.1117   44813.6986   47177.8974   49946.9571

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(10.6, 0.0751, 0.794, 2.43, 1.5, 49000.0, 1.43), 49.98326258767858, 4.126677523006329e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.0, 0.143, 0.76, 3.02, 3.31, 36400.0, 1.2), 49.981116338793065, 4.1268547274724036e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.7, 0.0606, 0.746, 2.87, 0.548, 47100.0, 1.19), 50.02197735068628, 4.1234836600130224e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.89, 0.165, 0.475, -0.385, 1.39, 44400.0, 1.07), 49.99772988585258, 4.1254834313079747e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.8, 0.0762, 0.643, -3.07, 1.45, 50100.0, 1.21), 49.95572297459494, 4.1289524796186546e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.2, 0.000566, 0.596, 2.67, 0.707, 47300.0, 1.07), 50.001972428681775, 4.125133394313466e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.8, 0.078, 0.708, -1.02, 3.69, 45800.0, 1.14), 50.0129060798769, 4.1242315717000235e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.9, 0.32, 0.63, -2.11, 2.53, 40900.0, 1.17), 49.995258764351135, 4.1256873420599718e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.9, 0.0403, 0.682, -0.739, 3.81, 46500.0, 1.14), 49.99451313740601, 4.1257488732852275e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.5, 0.176, 0.712, -0.0923, 4.21, 49100.0, 1.25), 50.01632464173231, 4.123949684919359e6)
 ⋮
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(7.22, 0.435, 0.202, 0.528, 0.881, 46600.0, 1.19), 50.009658883828976, 4.1244993637377867e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.1, 0.138, 0.534, -1.72, 1.77, 41000.0, 0.993), 50.005273791185274, 4.124861051824815e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.97, 0.169, 0.073, -1.22, 2.01, 44000.0, 1.1), 49.997315706883946, 4.1255176069121747e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.4, 0.0635, 0.669, -2.82, 0.598, 48700.0, 1.26), 50.0007816909682, 4.1252316318157604e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(16.9, 0.145, 0.969, 2.9, 0.446, 45900.0, 1.33), 49.99776399874401, 4.1254806165387295e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.6, 0.359, 0.591, -2.32, 2.24, 37400.0, 1.19), 50.00410163471426, 4.1249577435444114e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.5, 0.181, 0.571, -1.89, 1.85, 40800.0, 1.13), 49.98021643371329, 4.1269290324233975e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.0, 0.0487, 0.55, -1.91, 0.51, 36600.0, 1.18), 50.00528157590764, 4.124860409674685e6)
 Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.91, 0.165, 0.574, 0.901, 0.557, 44400.0, 1.13), 50.004450359097696, 4.124928976637956e6)

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.