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.93 seconds
Compute duration = 3.93 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.1846 0.1009 0.0046 485.3377 530.5081 1.0 ⋯
plx 50.0003 0.0201 0.0006 1219.6304 623.3267 1.0 ⋯
b_a 11.4725 2.3635 0.1982 129.2746 215.7430 1.0 ⋯
b_e 0.1609 0.1164 0.0069 271.1219 378.0819 1.0 ⋯
b_i 0.6009 0.1810 0.0194 115.2227 88.5121 1.0 ⋯
b_ωy -0.0073 0.7519 0.0671 136.0053 464.2955 1.0 ⋯
b_ωx 0.0009 0.6829 0.0401 328.4494 737.9782 1.0 ⋯
b_Ωy -0.0504 0.7455 0.1050 52.6589 408.1203 1.0 ⋯
b_Ωx -0.0717 0.6789 0.0960 52.5244 429.0248 1.0 ⋯
b_θy 0.0744 0.0097 0.0003 1056.3115 634.9045 0.9 ⋯
b_θx -1.0037 0.0993 0.0035 804.0251 491.5196 1.0 ⋯
b_ω -0.2244 1.8353 0.0974 392.8178 735.0555 1.0 ⋯
b_Ω -0.5728 1.7967 0.2833 65.9779 213.6939 1.0 ⋯
b_θ -1.4967 0.0071 0.0002 997.2839 377.0934 1.0 ⋯
b_tp 42800.9771 4853.1312 294.6710 281.2272 406.0620 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.0012 1.1130 1.1809 1.2549 1.3885
plx 49.9616 49.9860 50.0001 50.0137 50.0416
b_a 7.9291 9.7782 11.0883 12.9222 17.4595
b_e 0.0059 0.0619 0.1403 0.2436 0.4108
b_i 0.1539 0.5037 0.6299 0.7215 0.8999
b_ωy -1.0925 -0.7812 -0.0104 0.7584 1.1070
b_ωx -1.0546 -0.6358 0.0065 0.6189 1.0795
b_Ωy -1.1018 -0.7814 -0.1037 0.7241 1.0848
b_Ωx -1.0685 -0.6953 -0.1542 0.5293 1.0554
b_θy 0.0569 0.0673 0.0745 0.0807 0.0933
b_θx -1.2107 -1.0689 -0.9967 -0.9317 -0.8180
b_ω -3.0220 -2.0284 0.0249 1.1777 2.9856
b_Ω -3.0261 -2.2853 -0.4844 0.8230 2.9115
b_θ -1.5104 -1.5015 -1.4969 -1.4920 -1.4821
b_tp 32063.3704 39891.3015 43892.5005 46156.2293 49833.2793
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(12.6, 0.0106, 0.771, -1.07, 3.74, 45400.0, 1.35), 50.02380539072973, 4.123336847104888e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.68, 0.107, 0.617, 1.91, 4.74, 43700.0, 1.45), 50.006624056316035, 4.1247535480041653e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.1, 0.439, 0.608, 0.782, 5.52, 36600.0, 1.22), 49.995352510980496, 4.1256834813735527e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(20.0, 0.32, 0.97, -3.03, 0.5, 47400.0, 1.26), 50.02548193766749, 4.123198658176033e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(8.59, 0.292, 0.375, -1.99, 3.09, 45000.0, 1.21), 49.994193015216766, 4.1257791667368044e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.1, 0.296, 0.648, -1.96, 2.02, 40100.0, 1.16), 50.01701928134915, 4.1238962849774254e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.5, 0.143, 0.807, 1.19, 0.197, 42500.0, 1.32), 49.99207961178769, 4.1259535830824804e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.3, 0.0238, 0.606, -0.355, 3.83, 47800.0, 1.3), 49.98545903247466, 4.1265000660690805e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(10.9, 0.246, 0.575, 0.667, 6.22, 42100.0, 1.33), 50.00228424685817, 4.125111544538296e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(16.6, 0.486, 0.829, 0.641, 5.17, 28600.0, 1.12), 49.98868865984456, 4.126233464605578e6)
⋮
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.1, 0.0599, 0.597, 0.0886, 4.24, 49400.0, 1.05), 49.94397375372358, 4.1299276869138163e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.98, 0.117, 0.743, 1.6, 4.76, 42200.0, 1.22), 49.97628710551954, 4.1272573843769887e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.0, 0.169, 0.728, -2.41, 3.26, 41400.0, 1.28), 50.016945274854024, 4.123902386811686e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.42, 0.167, 0.499, -2.09, 3.65, 45400.0, 1.24), 50.01346349497431, 4.1241894799132817e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.9, 0.148, 0.609, 2.57, 3.36, 36600.0, 1.12), 49.974288404104165, 4.1274224523637313e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(14.4, 0.00103, 0.859, -2.35, 3.62, 40200.0, 1.27), 49.986097645742895, 4.126447346656731e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.8, 0.213, 0.613, 0.322, 3.96, 49300.0, 1.0), 50.01822690695009, 4.1237967188185e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.27, 0.163, 0.579, 2.41, 4.67, 44200.0, 1.26), 50.01455406020237, 4.1240995521367528e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.1, 0.116, 0.653, 0.587, 4.69, 39400.0, 1.35), 49.983334333670406, 4.1266754759306475e6)
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.