Multiple-axes plots

# -------- EXAMPLE DATA GENERATION --------------------
# Generate example data. Each condition will
# be a differently-sized and placed bivariate normal
# distribution.
np_rand = np.random.RandomState(seed=2022)
example_data = np.vstack((
    np_rand.multivariate_normal( # A
        [5, 15],[[1,0.2], [0,3]], size=(1000,)),
    np_rand.multivariate_normal( # B
        [15, 5], [[3,0], [0.2,1]], size=(1000,)),
    np_rand.multivariate_normal( # C
        [5, 5], [[1,0], [0,1]], size=(1000,)),
    np_rand.multivariate_normal( # D
        [15, 15], [[1,1], [0.2,-0.2]], size=(1000,)),
))
df = pd.DataFrame({
   'condition': (
        (['A'] * 1000) + (['B'] * 1000) +
        (['C'] * 1000) + (['D'] * 1000)
    ),
   'x': example_data[:,0],
   'y': example_data[:,1]
})
# -------- END EXAMPLE DATA GENERATION ---------------

# Specify a color palette
beach_towel = {
'A': '#225A9B',
'B': '#19D2BF',
'C': '#FFB133',
'D': '#FE484E',
}

# Create a multi-plot subplot layout, where we specify
# a one-row, two-column set of plots, where the first
# axis takes five times the space as the right axis.
_, axes = plt.subplots(
                ncols=2,
                sharey=False,
                gridspec_kw={'width_ratios': [5,1]})
# Use Seaborn to plot the joint distributions.
sns.kdeplot(data=df, x='x', y='y',
            hue='condition', palette=beach_towel,
            levels=8,
            ax=axes[0], legend=None)
# For each condition, calculate intrinsic and extrinsic noise
for condition in df.condition.unique():
    # subset the dataframe by condition
    df_subset = df[df.condition == condition]
    extrinsic_noise = np.var((df_subset.x + df_subset.y) / 2.0)
    intrinsic_noise = np.var((df_subset.x - df_subset.y) / 2.0)
    # use matplotlib to plot circles for each condition.
    # Jitter the x-axis locations so the dots don't overlap.
    axes[1].plot(
        np.random.uniform(-0.1,0.1),
        intrinsic_noise / extrinsic_noise,
        'o',
        color=beach_towel[condition])
# Modify axis titles
axes[0].set_title('Joint distribution')
axes[1].set_title('Noise\nratio', loc='left')
# Set uniform x and ytick labels
axes[0].set_xticks([5,10,15,20])
axes[0].set_yticks([5,10,15,20])
# Despine both axes, also removing the bottom spine
# on the right axis.
sns.despine(ax=axes[0])
sns.despine(ax=axes[1], bottom=True)
# Hide the right-most x axis.
axes[1].xaxis.set_visible(False)
# Scale the axis so our jittered points are visible.
axes[1].set_xlim([-0.3, 0.3])

(Source code)