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.57 seconds
Compute duration = 3.57 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.1828 0.0896 0.0061 208.6837 323.6384 1.0 ⋯
plx 50.0003 0.0200 0.0011 312.1717 399.1501 1.0 ⋯
b_a 12.2606 2.5214 0.4618 39.3117 32.4890 1.0 ⋯
b_e 0.2037 0.1377 0.0243 36.4253 88.2331 1.0 ⋯
b_i 0.6483 0.1357 0.0134 99.8399 269.7934 1.0 ⋯
b_ωy -0.1637 0.7365 0.1523 26.8737 336.4040 1.0 ⋯
b_ωx -0.0376 0.6687 0.0804 96.4125 443.1157 1.0 ⋯
b_Ωy -0.2028 0.7262 0.1300 36.8896 157.6594 1.0 ⋯
b_Ωx 0.0081 0.6619 0.1522 21.2818 136.9573 1.0 ⋯
b_θy 0.0739 0.0107 0.0010 113.3526 270.4690 0.9 ⋯
b_θx -0.9911 0.1086 0.0118 87.3234 162.4608 1.0 ⋯
b_ω -0.5569 1.9355 0.2547 65.5202 201.3293 1.0 ⋯
b_Ω -0.1570 2.0483 0.4115 34.3341 121.2582 1.0 ⋯
b_θ -1.4963 0.0069 0.0004 264.3629 422.3745 1.0 ⋯
b_tp 41012.3566 6046.0370 1323.3566 23.5781 23.1793 1.0 ⋯
2 columns omitted
Quantiles
parameters 2.5% 25.0% 50.0% 75.0% 97.5%
Symbol Float64 Float64 Float64 Float64 Float64
M 0.9979 1.1236 1.1831 1.2444 1.3577
plx 49.9605 49.9863 50.0006 50.0152 50.0356
b_a 8.4137 10.3207 11.7931 13.8060 17.5607
b_e 0.0103 0.0798 0.1918 0.2996 0.4546
b_i 0.3213 0.5653 0.6672 0.7421 0.8551
b_ωy -1.0753 -0.8259 -0.4147 0.5699 1.0750
b_ωx -1.0431 -0.6112 -0.1504 0.6323 1.0598
b_Ωy -1.1074 -0.8583 -0.3959 0.4932 1.0434
b_Ωx -1.0197 -0.5871 0.0585 0.5870 1.0535
b_θy 0.0555 0.0653 0.0732 0.0812 0.0950
b_θx -1.2337 -1.0640 -0.9819 -0.9156 -0.8142
b_ω -3.0288 -2.4691 -0.5507 1.0384 3.0269
b_Ω -2.9948 -2.2441 0.1035 1.6775 3.0265
b_θ -1.5091 -1.5016 -1.4962 -1.4919 -1.4817
b_tp 28548.8940 36712.2765 41764.9613 45885.4013 49980.4608
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(10.1, 0.0366, 0.63, -2.13, 4.31, 46200.0, 1.38), 49.997861477039216, 4.1254764485221864e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(15.3, 0.138, 0.727, 1.19, 3.42, 49800.0, 1.09), 49.9949329420653, 4.125718105053211e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.07, 0.3, 0.32, -2.29, 3.58, 46000.0, 1.28), 50.0027871287627, 4.1250700579718654e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.3, 0.0207, 0.817, 1.85, 3.52, 34400.0, 1.29), 49.99412702313152, 4.1257846127519007e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.4, 0.204, 0.561, -2.27, 3.11, 42700.0, 1.15), 49.9850555334262, 4.126533376802306e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.19, 0.176, 0.597, 0.724, 0.907, 45700.0, 1.3), 50.01463643048333, 4.1240927600602116e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.8, 0.0906, 0.612, 1.04, 0.367, 43400.0, 1.07), 49.96993623763164, 4.1277819331029044e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.1, 0.0362, 0.492, 1.88, 4.42, 42100.0, 1.18), 50.02448582831102, 4.1232807611041097e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.6, 0.133, 0.767, 0.714, 3.72, 49300.0, 1.15), 49.99975215290333, 4.1253204489739216e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.68, 0.19, 0.44, -2.48, 4.02, 45700.0, 1.11), 49.98946393912422, 4.126169471454701e6)
⋮
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.9, 0.292, 0.694, -2.41, 1.8, 36200.0, 1.19), 49.95940396760605, 4.1286521379186856e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.5, 0.463, 0.728, -2.51, 2.58, 34500.0, 1.25), 50.01876521745574, 4.1237523378129466e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.8, 0.166, 0.709, 3.04, 2.94, 36100.0, 1.22), 49.976470206449335, 4.1272422631677184e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.4, 0.258, 0.742, 1.52, 0.401, 45400.0, 1.21), 49.99335627797328, 4.1258482197739324e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.46, 0.261, 0.541, 1.12, 0.00862, 44300.0, 1.2), 49.99839333890625, 4.1254325634398917e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(15.1, 0.0527, 0.862, -2.11, 0.398, 49600.0, 1.3), 50.02796579602532, 4.1229939438470546e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.0, 0.164, 0.655, -2.99, 3.43, 39200.0, 1.02), 50.001611385320786, 4.1251670553272255e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.1, 0.3, 0.64, 3.03, 3.08, 34000.0, 1.07), 49.992262172011436, 4.1259385160505716e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.5, 0.112, 0.584, -0.225, 3.94, 48400.0, 1.12), 50.04433823985017, 4.1216450702459635e6)
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
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.