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.99 seconds
Compute duration = 3.99 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.1868 0.0924 0.0034 744.0602 795.9626 0.99 ⋯
plx 49.9997 0.0195 0.0006 959.2947 525.5966 0.99 ⋯
b_a 11.9315 2.5044 0.2210 92.0730 245.1912 1.02 ⋯
b_e 0.1620 0.1105 0.0067 252.5975 341.7935 1.02 ⋯
b_i 0.6385 0.1764 0.0236 68.0560 139.8406 1.03 ⋯
b_ωy 0.1045 0.7439 0.0681 145.3815 598.7533 1.00 ⋯
b_ωx -0.0293 0.6792 0.0434 278.9002 784.0029 1.00 ⋯
b_Ωy 0.1579 0.7368 0.0996 75.2006 367.4715 1.00 ⋯
b_Ωx 0.0865 0.6664 0.0858 68.6806 314.2985 1.00 ⋯
b_θy 0.0743 0.0099 0.0004 770.4779 656.0633 0.99 ⋯
b_θx -1.0018 0.0957 0.0034 784.1473 650.3573 0.99 ⋯
b_ω -0.2277 1.7063 0.1019 287.2212 559.1234 1.00 ⋯
b_Ω -0.2423 1.6425 0.2024 83.5352 310.5335 1.00 ⋯
b_θ -1.4967 0.0073 0.0002 946.4174 694.2048 1.00 ⋯
b_tp 42650.4702 5411.1826 358.7252 244.2135 398.3734 1.00 ⋯
2 columns omitted
Quantiles
parameters 2.5% 25.0% 50.0% 75.0% 97.5%
Symbol Float64 Float64 Float64 Float64 Float64
M 1.0073 1.1252 1.1821 1.2495 1.3710
plx 49.9596 49.9859 50.0006 50.0135 50.0354
b_a 8.0541 10.1212 11.6094 13.4091 17.4190
b_e 0.0057 0.0704 0.1455 0.2410 0.4008
b_i 0.2038 0.5523 0.6619 0.7616 0.9132
b_ωy -1.0708 -0.6716 0.2725 0.8142 1.0763
b_ωx -1.0635 -0.6654 -0.0638 0.6105 1.0240
b_Ωy -1.0310 -0.6058 0.3284 0.8551 1.0969
b_Ωx -1.0472 -0.4859 0.1880 0.6907 1.0694
b_θy 0.0559 0.0673 0.0737 0.0811 0.0944
b_θx -1.1962 -1.0675 -1.0002 -0.9317 -0.8356
b_ω -2.9682 -1.7831 -0.0833 0.9503 3.0013
b_Ω -2.9534 -1.9272 0.2225 0.9106 2.7145
b_θ -1.5101 -1.5019 -1.4968 -1.4917 -1.4815
b_tp 29967.2146 39329.4584 43827.6865 46493.8809 50052.9429
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(15.3, 0.0978, 0.796, -1.11, 0.157, 30400.0, 1.04), 50.01008367739242, 4.1244682038643383e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.5, 0.0426, 0.653, -2.99, 0.868, 49000.0, 1.13), 49.99479500170374, 4.125729488299148e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.9, 0.347, 0.677, 0.863, 5.68, 40000.0, 1.36), 49.99081325519081, 4.12605810085681e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.4, 0.255, 0.508, 0.264, 5.68, 37400.0, 1.16), 49.98029347918608, 4.126926547278029e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.4, 0.0105, 0.77, -0.205, 3.56, 46500.0, 1.2), 49.99368702874554, 4.1258209237778573e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.8, 0.261, 0.424, 0.89, 4.57, 34500.0, 0.97), 50.01796801847351, 4.1238180632171743e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.2, 0.0801, 0.687, -2.72, 0.869, 49500.0, 1.33), 50.01190400971923, 4.1243180815494405e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(9.73, 0.0349, 0.617, 0.0108, 1.33, 45000.0, 1.32), 49.98260390799346, 4.126735781506836e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.8, 0.153, 0.78, 3.13, 3.39, 36500.0, 1.12), 49.994667324166, 4.1257400246825395e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(16.1, 0.0951, 0.939, 0.339, 0.124, 35400.0, 1.27), 50.003192928971686, 4.125036581023824e6)
⋮
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.9, 0.195, 0.711, 0.181, 0.141, 38500.0, 1.19), 49.96784094073841, 4.1279550230042795e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(13.8, 0.055, 0.797, -1.07, 0.496, 34800.0, 1.12), 49.996636346875015, 4.1255775402357117e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(11.6, 0.134, 0.659, -2.57, 3.34, 41500.0, 1.2), 50.004986483604036, 4.124888626209939e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(15.5, 0.0259, 0.871, -1.29, 0.286, 31300.0, 1.17), 50.057177404665936, 4.120587909552679e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.1, 0.151, 0.782, -0.813, 3.97, 47300.0, 1.38), 50.01934335132485, 4.123704674634373e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.5, 0.0912, 0.782, -1.64, 3.38, 43700.0, 1.39), 49.99667899600953, 4.125574020955731e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(15.7, 0.00618, 0.914, -3.06, 0.323, 46200.0, 1.3), 50.014007935426974, 4.124144584979242e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(17.3, 0.391, 0.81, -2.99, 1.23, 49700.0, 1.1), 50.00971620887077, 4.12449851021975e6)
Visual{KepOrbit{Float64}, Float64}(KepOrbit(12.4, 0.0507, 0.731, -2.52, 3.53, 41200.0, 1.15), 49.988754112418, 4.1262280619384455e6)
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](55c92e34.png)
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.