import arviz as az
import bambi as bmb
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
"arviz-darkgrid")
plt.style.use(= 1234 SEED
Polynomial Regression
This example has been contributed by Tyler James Burch (@tjburch on GitHub).
# Temporary fix to make outputs cleaner
import warnings
"ignore") warnings.filterwarnings(
This example will discuss polynomial regression using Bambi. Unlike many other examples shown, there aren’t specific polynomial methods or families implemented in Bambi, most of the interesting behavior for polynomial regression occurs within the formula definition. Regardless, there are some nuances that are useful to be aware of.
This example uses the kinematic equations from classical mechanics as a backdrop. Specifically, an object in motion experiencing constant acceleration can be described by the following:
\[x_f = \frac{1}{2} a t^2 + v_0 t + x_0\]
Where \(x_0\) and \(x_f\) are the initial and final locations, \(v_0\) is the initial velocity, and \(a\) is acceleration.
A falling ball
First, we’ll consider a simple falling ball, released from 50 meters. In this situation, \(v_0 = 0\) \(m\)/\(s\), \(x_0 = 50\) \(m\) and \(a = g\), the acceleration due to gravity, \(-9.81\) \(m\)/\(s^2\). So dropping out the \(v_0 t\) component, the equation takes the form:
\[x_f = \frac{1}{2} g t^2 + x_0\]
We’ll start by simulating data for the first 2 seconds of motion. We will also assume some measurement error with a gaussian distribution of \(\sigma = 0.3\).
= -9.81 # acceleration due to gravity (m/s^2)
g = np.linspace(0, 2, 100) # time in seconds
t = 50
inital_height = 0.5 * g * t**2 + inital_height
x_falling
= np.random.default_rng(SEED)
rng = rng.normal(0, 0.3, x_falling.shape)
noise = x_falling + noise
x_obs_falling = pd.DataFrame({"t": t, "x": x_obs_falling})
df_falling
= plt.subplots(figsize=(10, 6))
fig, ax ="Observed Displacement", color="C0")
ax.scatter(t, x_obs_falling, label="True Function", color="C1")
ax.plot(t, x_falling, labelset(xlabel="Time (s)", ylabel="Displacement (m)")
ax.; ax.legend()
Casting the equation \(x_f = \frac{1}{2} g t^2 + x_0\) into a regression context, we let time (\(t\)) be the independent variable, and final location (\(x_f\)) be the response/dependent variable. This allows our coefficients to be proportional to \(g\) and \(x_0\). The intercept, \(\beta_0\) corresponds exactly to \(x_0\). Letting \(\beta_1 = \frac{1}{2} g\) then gives \(g = 2\beta_1\) when \(x_1 = t^2\), meaning we”re doing polynomial regression. We can put this into Bambi via the following, optionally including the + 1
to emphasize that we choose to include the coefficient.
= bmb.Model("x ~ I(t**2) + 1", df_falling)
model_falling = model_falling.fit(idata_kwargs={"log_likelihood": True}, random_seed=SEED) results_falling
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [sigma, Intercept, I(t ** 2)]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 2 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
The term I(t**2)
indicates to evaluate inside the I
. For including just the \(t^2\) term, you can express it any of the following ways:
I(t**2)
{t**2}
- Square the data directly, and pass it as a new column
To verify, we”ll fit the other two versions as well.
= bmb.Model(
model_falling_variation1 "x ~ {t**2} + 1", # Using {t**2} syntax
df_falling
)= model_falling_variation1.fit(random_seed=SEED)
results_variation1
= bmb.Model(
model_falling_variation2 "x ~ tsquared + 1", # Using data with the t variable squared
=t**2) # Creating the tsquared variable for use in the formula
df_falling.assign(tsquared
)= model_falling_variation2.fit(random_seed=SEED)
results_variation2
print("I{t**2} coefficient: ", round(results_falling.posterior["I(t ** 2)"].values.mean(), 4))
print("{t**2} coefficient: ", round(results_variation1.posterior["I(t ** 2)"].values.mean(), 4))
print("tsquared coefficient: ", round(results_variation2.posterior["tsquared"].values.mean(), 4))
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [sigma, Intercept, I(t ** 2)]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 2 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [sigma, Intercept, tsquared]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 2 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
I{t**2} coefficient: -4.8471
{t**2} coefficient: -4.8471
tsquared coefficient: -4.8471
Each of these provides identical results, giving -4.9, which is \(g/2\). This makes the acceleration exactly the \(-9.81\) \(m\)/\(s^2\) acceleration that generated the data. Looking at our model summary,
az.summary(results_falling)
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
I(t ** 2) | -4.847 | 0.028 | -4.901 | -4.798 | 0.000 | 0.000 | 3127.0 | 1408.0 | 1.0 |
Intercept | 49.961 | 0.051 | 49.854 | 50.050 | 0.001 | 0.001 | 3125.0 | 1233.0 | 1.0 |
sigma | 0.336 | 0.024 | 0.293 | 0.383 | 0.000 | 0.000 | 2550.0 | 1595.0 | 1.0 |
We see that both \(g/2 = -4.9\) (so \(g=-9.81\)) and the original height of \(x_0 = 50\) \(m\) are recovered, along with the injected noise.
We can then use the model to answer some questions, for example, when would the ball land? This would correspond to \(x_f = 0\).
\[ 0 = \frac{1}{2} g t^2 - x_0 \]
\[ t = \sqrt{2x_0 / g} \]
= results_falling.posterior["Intercept"].values.mean()
calculated_x0 = -2 * results_falling.posterior["I(t ** 2)"].values.mean()
calculated_g = np.sqrt(2 * calculated_x0 / calculated_g)
calculated_land print(f"The ball will land at {round(calculated_land, 2)} seconds")
The ball will land at 3.21 seconds
Or if we want to account for our measurement error and use the full posterior,
= results_falling.posterior["Intercept"].values
calculated_x0_posterior = -2 * results_falling.posterior["I(t ** 2)"].values
calculated_g_posterior = np.sqrt(2 * calculated_x0_posterior / calculated_g_posterior)
calculated_land_posterior = round(np.quantile(calculated_land_posterior, 0.025), 2)
lower_est = round(np.quantile(calculated_land_posterior, 0.975), 2)
upper_est print(f"The ball landing will be measured between {lower_est} and {upper_est} seconds")
The ball landing will be measured between 3.19 and 3.23 seconds
Projectile Motion
Next, instead of a ball strictly falling, instead imagine one thrown straight upward. In this case, we add the initial velocity back into the equation.
\[x_f = \frac{1}{2} g t^2 + v_0 t + x_0\]
We will envision the ball tossed upward, starting at 1.5 meters above ground level. It will be tossed at 7 m/s upward. It will also stop when hitting the ground.
= 7
v0 = 1.5
x0 = (1/2) * g * t**2 + v0 * t + x0
x_projectile = rng.normal(0, 0.2, x_projectile.shape)
noise = x_projectile + noise
x_obs_projectile = pd.DataFrame({"t": t, "tsq": t**2, "x": x_obs_projectile, "x_true": x_projectile})
df_projectile = df_projectile[df_projectile["x"] >= 0]
df_projectile
= plt.subplots(figsize=(10, 6))
fig, ax
="Observed Displacement", color="C0")
ax.scatter(df_projectile.t, df_projectile.x, label='True Function', color="C1")
ax.plot(df_projectile.t, df_projectile.x_true, labelset(xlabel="Time (s)", ylabel="Displacement (m)", ylim=(0, None))
ax.; ax.legend()
Modeling this using Bambi, we must include the linear term on time to capture the inital velocity. We’ll do the following regression,
\[x_f = \beta_0 + \beta_1 t + \beta_2 t^2\]
Which then maps the solved coefficents to the following: \(\beta_0 = x_0\), \(\beta_1 = v_0\), and \(\beta_2 = \frac{g}{2}\).
= bmb.Model("x ~ I(t**2) + t + 1", df_projectile)
model_projectile_all_terms = model_projectile_all_terms.fit(idata_kwargs={"log_likelihood": True}, target_accept=0.9, random_seed=SEED) fit_projectile_all_terms
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [sigma, Intercept, I(t ** 2), t]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 8 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
az.summary(fit_projectile_all_terms)
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
I(t ** 2) | -4.871 | 0.114 | -5.090 | -4.665 | 0.004 | 0.003 | 890.0 | 936.0 | 1.0 |
Intercept | 1.560 | 0.064 | 1.442 | 1.679 | 0.002 | 0.001 | 1093.0 | 1099.0 | 1.0 |
sigma | 0.202 | 0.017 | 0.171 | 0.234 | 0.000 | 0.000 | 1195.0 | 1102.0 | 1.0 |
t | 6.913 | 0.188 | 6.565 | 7.257 | 0.006 | 0.004 | 897.0 | 916.0 | 1.0 |
= az.hdi(fit_projectile_all_terms.posterior, hdi_prob=0.95)
hdi print(f"Initial height: {hdi['Intercept'].sel(hdi='lower'):.2f} to {hdi['Intercept'].sel(hdi='higher'):.2f} meters (True: {x0} m)")
print(f"Initial velocity: {hdi['t'].sel(hdi='lower'):.2f} to {hdi['t'].sel(hdi='higher'):.2f} meters per second (True: {v0} m/s)")
print(f"Acceleration: {2*hdi['I(t ** 2)'].sel(hdi='lower'):.2f} to {2*hdi['I(t ** 2)'].sel(hdi='higher'):.2f} meters per second squared (True: {g} m/s^2)")
Initial height: 1.44 to 1.68 meters (True: 1.5 m)
Initial velocity: 6.53 to 7.26 meters per second (True: 7 m/s)
Acceleration: -10.17 to -9.30 meters per second squared (True: -9.81 m/s^2)
We once again are able to recover all our input parameters.
In addition to directly calculating all terms, to include all polynomial terms up to a given degree you can use the poly
keyword. We don’t do that in this notebook for two reasons. First, by default it orthogonalizes the terms making it ill-suited to this example since the coefficients have physical meaning. For more information on the orthogonalization, please see the orthogonal polynomial notebook. The orthogonalization process can be disabled by the raw
argument of poly
, but we still elect not to use it because in later examples we decide to use different effects on the \(t\) term vs the \(t^2\) term, and doing so is not easy when using poly
. However, just to show that the results match when using the raw = True
argument, we’ll fit the same model as above.
= bmb.Model("x ~ poly(t, 2, raw=True)", df_projectile)
model_poly_raw = model_poly_raw.fit(idata_kwargs={"log_likelihood": True}, random_seed=SEED)
fit_poly_raw az.summary(fit_poly_raw)
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (2 chains in 2 jobs)
NUTS: [sigma, Intercept, poly(t, 2, raw=True)]
Sampling 2 chains for 1_000 tune and 1_000 draw iterations (2_000 + 2_000 draws total) took 7 seconds.
We recommend running at least 4 chains for robust computation of convergence diagnostics
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
Intercept | 1.562 | 0.064 | 1.446 | 1.686 | 0.002 | 0.001 | 1220.0 | 1319.0 | 1.0 |
poly(t, 2, raw=True)[0] | 6.910 | 0.183 | 6.538 | 7.237 | 0.006 | 0.004 | 977.0 | 1077.0 | 1.0 |
poly(t, 2, raw=True)[1] | -4.869 | 0.110 | -5.084 | -4.670 | 0.003 | 0.002 | 1004.0 | 1184.0 | 1.0 |
sigma | 0.202 | 0.017 | 0.171 | 0.234 | 0.000 | 0.000 | 1399.0 | 1215.0 | 1.0 |
We see the same results, where poly(t, 2, raw=True)[0]
corresponds to the coefficient on \(t\) (\(v_0\) in our example), and poly(t, 2, raw=True)[1]
is the coefficient on \(t^2\) (\(\frac{g}{2}\)).
Measuring gravity on a new planet
In the next example, you’ve been recruited to join the space program as a research scientist, looking to directly measure the gravity on a new planet, PlanetX. You don’t know anything about this planet or it’s safety, so you have time for one, and only one, throw of a ball. However, you’ve perfected your throwing mechanics, and can achieve the same initial velocity wherever you are. To baseline, you make a toss on planet Earth, warm up your spacecraft and stop at Mars to make a toss, then travel far away, and make a toss on PlanetX.
First we simulate data for this experiment.
def simulate_throw(v0, g, noise_std, time_step=0.25, max_time=10, seed=1234):
= np.random.default_rng(seed)
rng = np.arange(0, max_time, time_step)
times = v0 * times - 0.5 * g * times**2
heights = heights + rng.normal(0, noise_std, len(times))
heights_with_noise = heights_with_noise >= 0
valid_indices return times[valid_indices], heights_with_noise[valid_indices], heights[valid_indices]
# Define the parameters
= 20 # Initial velocity (m/s)
v0 = {"Earth": 9.81, "Mars": 3.72, "PlanetX": 6.0} # Gravitational acceleration (m/s^2)
g_planets = 1.5 # Standard deviation for noise
noise_std
# Generate data
= []
records for planet, g in g_planets.items():
= simulate_throw(v0, g, noise_std)
times, heights, heights_true for time, height, height_true in zip(times, heights, heights_true):
records.append([planet, time, height, height_true])
# Convert to a DataFrame
= pd.DataFrame(records, columns=["Planet", "Time", "Height", "Height_true"])
df "Planet"] = df["Planet"].astype("category") df[
And drawing those trajectories,
= plt.subplots(figsize=(10, 6))
fig, ax
for i, planet in enumerate(df["Planet"].cat.categories):
= df[df["Planet"] == planet]
subset "Time"], subset["Height_true"], alpha=0.7, color=f"C{i}")
ax.plot(subset["Time"], subset["Height"], alpha=0.7, label=planet, color=f"C{i}")
ax.scatter(subset[
set(
ax.="Time (seconds)", ylabel="Height (meters)", title="Trajectory Comparison", ylim=(0, None)
xlabel
)="Planet"); ax.legend(title
We now aim to model this data. We again use the folowing equation (calling displacement \(h\) for height):
\[ h = \frac{1}{2} g_{p} t^2 + v_{0} t \]
where \(g_p\) now has a subscript to indicate the planet that we’re throwing from.
In Bambi, we’ll do the following:
Height ~ I(Time**2):Planet + Time + 0
which corresponds one-to-one with the above formula. The intercept is eliminated since we start from \(x=0\).
= bmb.Model("Height ~ I(Time**2):Planet + Time + 0", df)
planet_model
planet_model.build() planet_model.graph()
= planet_model.fit(chains=4, idata_kwargs={"log_likelihood": True}, random_seed=SEED) planet_fit
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 2 jobs)
NUTS: [sigma, I(Time ** 2):Planet, Time]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 13 seconds.
The model has fit. Let’s look at how we did recovering the simulated parameters.
az.summary(planet_fit)
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
I(Time ** 2):Planet[Earth] | -4.995 | 0.076 | -5.139 | -4.855 | 0.002 | 0.001 | 1659.0 | 1779.0 | 1.0 |
I(Time ** 2):Planet[Mars] | -1.883 | 0.022 | -1.926 | -1.843 | 0.001 | 0.000 | 1229.0 | 1533.0 | 1.0 |
I(Time ** 2):Planet[PlanetX] | -3.016 | 0.036 | -3.083 | -2.949 | 0.001 | 0.001 | 1352.0 | 1739.0 | 1.0 |
Time | 20.123 | 0.165 | 19.812 | 20.429 | 0.005 | 0.003 | 1235.0 | 1703.0 | 1.0 |
sigma | 1.756 | 0.144 | 1.516 | 2.043 | 0.003 | 0.002 | 1806.0 | 2076.0 | 1.0 |
Getting the gravities back to the physical value,
= az.hdi(planet_fit.posterior, hdi_prob=0.95)
hdi print(f"g for Earth: {2*hdi['I(Time ** 2):Planet'].sel({'I(Time ** 2):Planet_dim':'Earth', 'hdi':'lower'}):.2f} to {2*hdi['I(Time ** 2):Planet'].sel({'I(Time ** 2):Planet_dim':'Earth', 'hdi':'higher'}):.2f} meters (True: -9.81 m)")
print(f"g for Mars: {2*hdi['I(Time ** 2):Planet'].sel({'I(Time ** 2):Planet_dim':'Mars', 'hdi':'lower'}):.2f} to {2*hdi['I(Time ** 2):Planet'].sel({'I(Time ** 2):Planet_dim':'Mars', 'hdi':'higher'}):.2f} meters (True: -3.72 m)")
print(f"g for PlanetX: {2*hdi['I(Time ** 2):Planet'].sel({'I(Time ** 2):Planet_dim':'PlanetX', 'hdi':'lower'}):.2f} to {2*hdi['I(Time ** 2):Planet'].sel({'I(Time ** 2):Planet_dim':'PlanetX', 'hdi':'higher'}):.2f} meters (True: -6.0 m)")
print(f"Initial velocity: {hdi['Time'].sel(hdi='lower'):.2f} to {hdi['Time'].sel(hdi='higher'):.2f} meters per second (True: 20 m/s)")
g for Earth: -10.30 to -9.70 meters (True: -9.81 m)
g for Mars: -3.85 to -3.68 meters (True: -3.72 m)
g for PlanetX: -6.18 to -5.90 meters (True: -6.0 m)
Initial velocity: 19.79 to 20.43 meters per second (True: 20 m/s)
We can see that we’re pretty close to recovering most the parameters, but the fit isn’t great. Plotting the posteriors for \(g\) agsint the true values,
= -2 * planet_fit.posterior["I(Time ** 2):Planet"].sel({"I(Time ** 2):Planet_dim": "Earth"})
earth_posterior = -2 * planet_fit.posterior["I(Time ** 2):Planet"].sel({"I(Time ** 2):Planet_dim": "PlanetX"})
planetx_posterior = -2 * planet_fit.posterior["I(Time ** 2):Planet"].sel({"I(Time ** 2):Planet_dim": "Mars"})
mars_posterior
= plt.subplots(1, 3, figsize=(12, 6))
fig, axs =9.81, ax=axs[0])
az.plot_posterior(earth_posterior, ref_val0].set_title("Posterior $g$ on Earth")
axs[=3.72, ax=axs[1])
az.plot_posterior(mars_posterior, ref_val1].set_title("Posterior $g$ on Mars")
axs[=6.0, ax=axs[2])
az.plot_posterior(planetx_posterior, ref_val2].set_title("Posterior $g$ on PlanetX"); axs[
The fit seems to work, more or less, but certainly could be improved.
Adding a prior
But, we can do better! We have a very good idea of the acceleration due to gravity on Earth and Mars, so why not use that information? From an experimental standpoint, we can consider these throws from a calibration mindset, allowing us to get some information on the resolution of our detector, and our throwing apparatus. The model will spend considerably less time trying pin down those parameters, and will better explore other parameters with already good values of the \(g\) terms locked in.
For Earth, at the extremes, \(g\) takes values as low as 9.78 \(m\)/\(s^2\) (at the Equator) up to 9.83 (at the Poles). So we can add a very strong prior,
\[ g_{\text{Earth}} \sim \text{Normal}(-9.81, 0.025) \]
For Mars, we know the mean value is about 3.72 \(m\)/\(s^2\). There’s less information on local variation readily available by a cursory search, however we know that the radius of Mars is about half that of Earth, so \(\sigma = \frac{0.025}{2} = 0.0125\) might make sense, but to be conservative we’ll round that up to \(\sigma = 0.02\).
\[ g_{\text{Mars}} \sim \text{Normal}(-3.72, 0.02) \]
For PlanetX, we must use a very loose prior. We might say that we know the ball took longer to fall than Earth, but not as long as on Mars, so we can split the difference. Then set a very wide \(\sigma\) value.
\[ g_{\text{PlanetX}} \sim \text{Normal}(\frac{-9.81 - 3.72}{2}, 3) = \text{Normal}(-6.77, 3) \]
Since these correspond to \(g/2\), we’ll divide all values by 2 when putting them into Bambi. Additionally, we know the balls landed eventually, so \(g\) must be negative. We’ll truncate the upper limit of the distribution at 0.
Now, for defining this in Bambi, the term of interest is I(Time ** 2):Planet
. Often, you set one prior that applies to all groups, however, if you want to set each group individually, you can pass a list to the bmb.Prior
definition. The broadcasting rules from PyMC apply here, so it could equivalently take a numpy array. You’ll notice that the priors are passed alphabetically by group name.
= {
priors "I(Time ** 2):Planet": bmb.Prior(
"TruncatedNormal",
=[
mu-9.81/2, # Earth
-3.72/2, # Mars
-6.77/2 # PlanetX
],=[
sigma0.025/2, # Earth
0.02/2, # Mars
3/2 # PlanetX
],=[0, 0, 0]
upper
)}
= bmb.Model(
planet_model_with_prior 'Height ~ I(Time**2):Planet + Time + 0',
df,=priors
priors
)
planet_model_with_prior.build()= planet_model_with_prior.prior_predictive()
idata ="stats") az.summary(idata.prior, kind
Sampling: [Height, I(Time ** 2):Planet, Time, sigma]
mean | sd | hdi_3% | hdi_97% | |
---|---|---|---|---|
I(Time ** 2):Planet[Earth] | -4.905 | 0.012 | -4.927 | -4.881 |
I(Time ** 2):Planet[Mars] | -1.860 | 0.010 | -1.875 | -1.839 |
I(Time ** 2):Planet[PlanetX] | -3.407 | 1.440 | -6.275 | -0.874 |
Time | -0.552 | 15.200 | -23.585 | 30.847 |
mu[0] | -0.445 | 3.800 | -6.203 | 7.405 |
... | ... | ... | ... | ... |
mu[77] | -115.814 | 100.895 | -286.910 | 72.314 |
mu[78] | -125.960 | 106.379 | -309.774 | 69.448 |
mu[79] | -136.532 | 111.989 | -330.351 | 68.152 |
mu[80] | -147.529 | 117.726 | -351.004 | 67.998 |
sigma | 14.737 | 13.655 | 0.044 | 37.748 |
86 rows × 4 columns
Here we’ve sampled the prior predictive and can see that our priors are correctly specified to the associated planets.
Next we fit the model.
= planet_model_with_prior.fit(chains=4, idata_kwargs={"log_likelihood": True}, random_seed=SEED)
planet_fit_with_prior
az.summary(planet_fit_with_prior)="pps"); planet_model_with_prior.predict(planet_fit_with_prior, kind
Auto-assigning NUTS sampler...
Initializing NUTS using jitter+adapt_diag...
Multiprocess sampling (4 chains in 2 jobs)
NUTS: [sigma, I(Time ** 2):Planet, Time]
Sampling 4 chains for 1_000 tune and 1_000 draw iterations (4_000 + 4_000 draws total) took 17 seconds.
0:5] az.summary(planet_fit_with_prior)[
mean | sd | hdi_3% | hdi_97% | mcse_mean | mcse_sd | ess_bulk | ess_tail | r_hat | |
---|---|---|---|---|---|---|---|---|---|
I(Time ** 2):Planet[Earth] | -4.907 | 0.012 | -4.928 | -4.884 | 0.000 | 0.000 | 4359.0 | 3199.0 | 1.0 |
I(Time ** 2):Planet[Mars] | -1.862 | 0.008 | -1.878 | -1.846 | 0.000 | 0.000 | 2657.0 | 2611.0 | 1.0 |
I(Time ** 2):Planet[PlanetX] | -2.985 | 0.022 | -3.026 | -2.944 | 0.000 | 0.000 | 3121.0 | 2938.0 | 1.0 |
Time | 19.958 | 0.074 | 19.830 | 20.105 | 0.001 | 0.001 | 2514.0 | 2572.0 | 1.0 |
sigma | 1.753 | 0.140 | 1.508 | 2.027 | 0.002 | 0.002 | 3667.0 | 3010.0 | 1.0 |
We see some improvements here! Off the cuff, these look better, you’ll notice the \(v_0\) coefficient on Time
covers the true value of 20 m/s.
Now taking a look at the effects before and after adding the prior on the gravities,
= -2 * planet_fit_with_prior.posterior["I(Time ** 2):Planet"].sel({"I(Time ** 2):Planet_dim": "Earth"})
earth_posterior_2 = -2 * planet_fit_with_prior.posterior["I(Time ** 2):Planet"].sel({"I(Time ** 2):Planet_dim": "Mars"})
mars_posterior_2 = -2 * planet_fit_with_prior.posterior["I(Time ** 2):Planet"].sel({"I(Time ** 2):Planet_dim": "PlanetX"})
planetx_posterior_2
= plt.subplots(2, 3, figsize=(12, 6), sharex='col')
fig, axs =9.81, ax=axs[0,0])
az.plot_posterior(earth_posterior, ref_val0,0].set_title("Earth $g$ - No Prior")
axs[=3.72, ax=axs[0,1])
az.plot_posterior(mars_posterior, ref_val0,1].set_title("Mars $g$ - No Prior")
axs[=6.0, ax=axs[0,2])
az.plot_posterior(planetx_posterior, ref_val0,2].set_title("PlanetX $g$ - No Prior")
axs[
=9.81, ax=axs[1,0])
az.plot_posterior(earth_posterior_2, ref_val1,0].set_title("Earth $g$ - Priors Used")
axs[=3.72, ax=axs[1,1])
az.plot_posterior(mars_posterior_2, ref_val1,1].set_title("Mars $g$ - Priors Used")
axs[=6.0, ax=axs[1,2])
az.plot_posterior(planetx_posterior_2, ref_val1,2].set_title("PlanetX $g$ - Priors Used"); axs[
Adding the prior gives smaller uncertainties for Earth and Mars by design, however, we can see the estimate for PlanetX has also considerably improved by injecting our knowledge into the model.
%load_ext watermark
%watermark -n -u -v -iv -w
Last updated: Sun May 26 2024
Python implementation: CPython
Python version : 3.11.9
IPython version : 8.24.0
pandas : 2.2.2
numpy : 1.26.4
arviz : 0.18.0
matplotlib: 3.8.4
bambi : 0.13.1.dev39+gb7d6a6cb
Watermark: 2.4.3