The difference between amateur and professional visualization is not aesthetics but statistical integrity: showing uncertainty, avoiding perceptual tricks, ensuring accessibility, and documenting what you've done. This skill enforces visualization practices that meet publication standards while preventing common manipulations.
FUNDAMENTAL RULES:
- Every estimate needs uncertainty bands
- Every axis needs units
- Every color scheme needs accessibility testing
- Every truncated axis needs justification
- Every plot needs to be reproducible
@app.cell
def create_publication_figure(df, x_var, y_var, group_var=None):
"""
Every visualization must meet publication standards.
This template enforces labeling, uncertainty, accessibility, and
reproducibility requirements that prevent common mistakes.
"""
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import os
from matplotlib import cm
# MANDATORY: Set publication-quality defaults
plt.rcParams.update({
'font.size': 11,
'font.family': 'sans-serif',
'axes.labelsize': 12,
'axes.titlesize': 13,
'xtick.labelsize': 10,
'ytick.labelsize': 10,
'legend.fontsize': 10,
'figure.titlesize': 14,
'axes.linewidth': 1.5,
'axes.grid': True,
'grid.alpha': 0.3,
})
# MANDATORY: Colorblind-safe palette
# Source: Wong, B. (2011) Nature Methods 8, 441
colorblind_colors = [
'#0173B2', # Blue
'#DE8F05', # Orange
'#029E73', # Green
'#CC78BC', # Light purple
'#ECE133', # Yellow
'#56B4E9', # Light blue
'#F0E442', # Light yellow
]
# Create figure with specific size for journal column width
# Single column: 3.5", double column: 7"
fig, ax = plt.subplots(figsize=(7, 5), dpi=144)
# Get data statistics for annotations
n_obs = len(df)
x_mean = df[x_var].mean()
y_mean = df[y_var].mean()
if group_var is None:
# Single group with confidence band
ax.scatter(df[x_var], df[y_var], alpha=0.6, s=50,
color=colorblind_colors[0], edgecolor='black', linewidth=0.5)
# Add regression line with confidence interval
from scipy import stats
slope, intercept, r_value, p_value, std_err = stats.linregress(df[x_var], df[y_var])
x_line = np.linspace(df[x_var].min(), df[x_var].max(), 100)
y_line = slope * x_line + intercept
# Calculate confidence interval for regression line
from scipy.stats import t
predict_se = std_err * np.sqrt(1/n_obs + (x_line - x_mean)**2 / ((df[x_var] - x_mean)**2).sum())
margin = t.ppf(0.975, n_obs - 2) * predict_se
ax.plot(x_line, y_line, color=colorblind_colors[0], linewidth=2,
label=f'Fit (R² = {r_value**2:.3f})')
ax.fill_between(x_line, y_line - margin, y_line + margin,
alpha=0.2, color=colorblind_colors[0], label='95% CI')
# Add statistical annotation
ax.text(0.05, 0.95, f'n = {n_obs}\np = {p_value:.3f}',
transform=ax.transAxes, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
else:
# Multiple groups with distinct colors and markers
markers = ['o', 's', '^', 'D', 'v', '<', '>']
groups = df[group_var].unique()
for i, group in enumerate(groups):
subset = df[df[group_var] == group]
color = colorblind_colors[i % len(colorblind_colors)]
marker = markers[i % len(markers)]
# Plot with confidence intervals
x_vals = subset.groupby(x_var)[y_var].mean()
x_sems = subset.groupby(x_var)[y_var].sem()
ax.errorbar(x_vals.index, x_vals.values, yerr=1.96*x_sems.values,
marker=marker, markersize=8, linewidth=2, capsize=5,
color=color, label=f'{group} (n={len(subset)})',
alpha=0.8)
# MANDATORY: Complete axis labeling with units
# Extract units from variable names if formatted as "variable (unit)"
x_label = x_var if '(' in x_var else f"{x_var} (units)"
y_label = y_var if '(' in y_var else f"{y_var} (units)"
ax.set_xlabel(x_label, fontweight='bold')
ax.set_ylabel(y_label, fontweight='bold')
# MANDATORY: Informative title
if group_var:
ax.set_title(f'{y_var} vs {x_var} by {group_var}\n(Mean ± 95% CI)',
fontweight='bold', pad=20)
else:
ax.set_title(f'{y_var} vs {x_var}\n(with 95% Confidence Band)',
fontweight='bold', pad=20)
# MANDATORY: Grid for readability
ax.grid(True, alpha=0.3, linestyle='--')
# MANDATORY: Legend with sample sizes
if group_var or True: # Always show legend for clarity
ax.legend(loc='best', frameon=True, fancybox=True, shadow=True)
# Check if y-axis should start at zero (for ratios, percentages, counts)
if any(keyword in y_var.lower() for keyword in ['percent', 'ratio', 'proportion', 'count']):
y_min, y_max = ax.get_ylim()
if y_min > 0:
ax.set_ylim(bottom=0, top=y_max * 1.1)
ax.annotate('Note: Y-axis starts at zero', xy=(0.5, -0.15),
xycoords='axes fraction', ha='center', fontsize=9, style='italic')
# Add data source and timestamp for reproducibility
fig.text(0.99, 0.01, f'Data: {n_obs} observations | Generated: {pd.Timestamp.now().strftime("%Y-%m-%d")}',
ha='right', fontsize=8, style='italic', alpha=0.7)
# Tight layout to prevent label cutoff
plt.tight_layout()
# MANDATORY: Save in multiple formats
os.makedirs("./images", exist_ok=True)
# Screen viewing (PNG at 144 DPI)
plt.savefig("./images/figure_screen.png", dpi=144, bbox_inches="tight")
# Publication (PDF vector format)
plt.savefig("./images/figure_publication.pdf", bbox_inches="tight")
# High-res for presentations (PNG at 300 DPI)
plt.savefig("./images/figure_highres.png", dpi=300, bbox_inches="tight")
print("Figure saved in three formats:")
print(" ./images/figure_screen.png (144 DPI for screens)")
print(" ./images/figure_publication.pdf (vector for journals)")
print(" ./images/figure_highres.png (300 DPI for presentations)")
# CRITICAL: Return figure for inline display in notebook
# Do NOT call plt.show() - it prevents inline display and blocks execution
# Do NOT call plt.close() - this would prevent display
# MUST return figure object (fig, plt.gcf(), or plt.gca()) as last expression
return fig,
</code_template>
</implementation_pattern>
<examples>
<example context="treatment_effects" difficulty="basic">
<description>Visualizing treatment effects with proper uncertainty bands</description>
<code>
```python
@app.cell
def plot_treatment_effects():
#Treatment effects must show uncertainty to enable
# proper inference. This example shows the gold standard for
# presenting experimental results with multiple treatments.
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import os
# Simulated treatment effects from an experiment
treatments = ['Control', 'Treatment A', 'Treatment B', 'Treatment C']
effects = [0, 2.3, 3.7, 1.8]
std_errors = [0.5, 0.6, 0.8, 0.7]
sample_sizes = [102, 98, 95, 101]
p_values = [1.000, 0.001, 0.0001, 0.042]
# MANDATORY: Colorblind-safe palette
colors = ['#808080', '#0173B2', '#029E73', '#DE8F05']
fig, ax = plt.subplots(figsize=(10, 6), dpi=144)
# Calculate 95% confidence intervals
ci_lower = [e - 1.96*se for e, se in zip(effects, std_errors)]
ci_upper = [e + 1.96*se for e, se in zip(effects, std_errors)]
# Create coefficient plot (forest plot style)
y_positions = range(len(treatments))
for i, (treatment, effect, lower, upper, n, p, color) in enumerate(
zip(treatments, effects, ci_lower, ci_upper, sample_sizes, p_values, colors)
):
# Plot point estimate
ax.scatter(effect, i, s=150, color=color, zorder=3,
edgecolors='black', linewidth=1.5)
# Plot confidence interval
ax.plot([lower, upper], [i, i], linewidth=3, color=color, alpha=0.7)
# Add caps to CI
cap_width = 0.05
ax.plot([lower, lower], [i-cap_width, i+cap_width], linewidth=2, color=color)
ax.plot([upper, upper], [i-cap_width, i+cap_width], linewidth=2, color=color)
# Add text annotations with sample size and p-value
significance = '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else 'ns'
ax.text(upper + 0.2, i, f'n={n}, p={p:.3f} {significance}',
verticalalignment='center', fontsize=9)
# MANDATORY: Reference line at zero
ax.axvline(0, color='red', linestyle='--', alpha=0.5, linewidth=1.5,
label='No effect')
# Shade region of practical significance (example: ±0.5)
ax.axvspan(-0.5, 0.5, alpha=0.1, color='gray',
label='Region of practical equivalence')
# MANDATORY: Complete labeling
ax.set_yticks(y_positions)
ax.set_yticklabels(treatments)
ax.set_xlabel('Treatment Effect (units of outcome)', fontweight='bold', fontsize=12)
ax.set_title('Treatment Effects with 95% Confidence Intervals\nRelative to Control Group',
fontweight='bold', fontsize=14)
# Add grid for easier reading
ax.grid(True, axis='x', alpha=0.3, linestyle='--')
# Legend
ax.legend(loc='upper right', frameon=True)
# Add interpretation guide
fig.text(0.12, 0.02,
'Interpretation: Points show estimates, bars show 95% CI. ' +
'Effects excluding zero are statistically significant at α=0.05.',
fontsize=9, style='italic', wrap=True)
# Statistical significance legend
fig.text(0.88, 0.02,
'*** p<0.001, ** p<0.01, * p<0.05, ns: not significant',
ha='right', fontsize=8, style='italic')
plt.tight_layout(rect=[0, 0.05, 1, 1])
# Save
os.makedirs("./images", exist_ok=True)
plt.savefig("./images/treatment_effects.png", dpi=144, bbox_inches="tight")
plt.savefig("./images/treatment_effects.pdf", bbox_inches="tight")
print("Treatment effect plot created with:")
print(" - 95% confidence intervals")
print(" - Sample sizes and p-values")
print(" - Colorblind-safe colors")
print(" - Reference line at zero")
print(" - Region of practical equivalence")
# Return figure for inline display
return plt.gcf(),
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from scipy import stats
import os
# Generate example time series data
np.random.seed(42)
dates = pd.date_range('2020-01-01', '2023-12-31', freq='M')
n_points = len(dates)
# Pre-intervention trend
pre_intervention = 36 # Month 36 is intervention
trend_pre = 100 + 2 * np.arange(pre_intervention)
noise_pre = np.random.normal(0, 10, pre_intervention)
# Post-intervention (level shift + trend change)
level_shift = 20
trend_change = -1.5
trend_post = (100 + 2 * pre_intervention + level_shift +
trend_change * np.arange(n_points - pre_intervention))
noise_post = np.random.normal(0, 12, n_points - pre_intervention)
# Combine
values = np.concatenate([trend_pre + noise_pre, trend_post + noise_post])
df = pd.DataFrame({
'date': dates,
'value': values,
'period': ['Pre' if i < pre_intervention else 'Post' for i in range(n_points)]
})
# Calculate rolling mean and std for uncertainty band
df['rolling_mean'] = df['value'].rolling(window=3, center=True).mean()
df['rolling_std'] = df['value'].rolling(window=3, center=True).std()
# VISUALIZATION
fig, ax = plt.subplots(figsize=(14, 7), dpi=144)
# Different colors for pre/post periods
pre_color = '#0173B2' # Blue
post_color = '#DE8F05' # Orange
# Plot pre-intervention
pre_data = df[df['period'] == 'Pre']
ax.plot(pre_data['date'], pre_data['value'],
marker='o', markersize=4, linewidth=1.5,
color=pre_color, label='Pre-intervention', alpha=0.8)
# Plot post-intervention
post_data = df[df['period'] == 'Post']
ax.plot(post_data['date'], post_data['value'],
marker='s', markersize=4, linewidth=1.5,
color=post_color, label='Post-intervention', alpha=0.8)
# Add uncertainty bands (±1.96 SE)
ax.fill_between(df['date'],
df['rolling_mean'] - 1.96*df['rolling_std'].fillna(0),
df['rolling_mean'] + 1.96*df['rolling_std'].fillna(0),
alpha=0.2, color='gray', label='95% CI (rolling)')
# MANDATORY: Mark intervention point
intervention_date = dates[pre_intervention]
ax.axvline(intervention_date, color='red', linestyle='--', linewidth=2,
label='Intervention', alpha=0.7)
# Add shaded region for intervention period
ax.axvspan(intervention_date, dates[-1], alpha=0.1, color='yellow')
# Fit trend lines for each period
from sklearn.linear_model import LinearRegression
# Pre-intervention trend
X_pre = np.arange(len(pre_data)).reshape(-1, 1)
model_pre = LinearRegression().fit(X_pre, pre_data['value'])
trend_line_pre = model_pre.predict(X_pre)
ax.plot(pre_data['date'], trend_line_pre, '--',
color=pre_color, linewidth=2, alpha=0.5)
# Post-intervention trend
X_post = np.arange(len(post_data)).reshape(-1, 1)
model_post = LinearRegression().fit(X_post, post_data['value'])
trend_line_post = model_post.predict(X_post)
ax.plot(post_data['date'], trend_line_post, '--',
color=post_color, linewidth=2, alpha=0.5)
# Add annotations with effect sizes
pre_slope = model_pre.coef_[0]
post_slope = model_post.coef_[0]
level_change = trend_line_post[0] - trend_line_pre[-1]
# Text box with results
textstr = f'Pre-trend: {pre_slope:.2f}/month\n'
textstr += f'Post-trend: {post_slope:.2f}/month\n'
textstr += f'Level change: {level_change:.1f} units\n'
textstr += f'Trend change: {post_slope - pre_slope:.2f}/month'
props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)
ax.text(0.02, 0.98, textstr, transform=ax.transAxes, fontsize=10,
verticalalignment='top', bbox=props)
# MANDATORY: Complete labeling
ax.set_xlabel('Date', fontweight='bold', fontsize=12)
ax.set_ylabel('Outcome Measure (units)', fontweight='bold', fontsize=12)
ax.set_title('Interrupted Time Series Analysis\nWith Intervention Effect Quantification',
fontweight='bold', fontsize=14)
# Format x-axis dates
import matplotlib.dates as mdates
ax.xaxis.set_major_locator(mdates.YearLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))
ax.xaxis.set_minor_locator(mdates.MonthLocator([1, 4, 7, 10]))
# Grid and legend
ax.grid(True, alpha=0.3, linestyle='--')
ax.legend(loc='upper left', frameon=True)
# Add note about statistical testing
fig.text(0.5, 0.01,
'Note: Formal interrupted time series analysis required for causal inference',
ha='center', fontsize=9, style='italic')
plt.tight_layout()
# Save
os.makedirs("./images", exist_ok=True)
plt.savefig("./images/time_series_intervention.png", dpi=144, bbox_inches="tight")
plt.savefig("./images/time_series_intervention.pdf", bbox_inches="tight")
print("Time series plot created with:")
print(" - Clear intervention marking")
print(" - Pre/post trend lines")
print(" - Uncertainty bands")
print(" - Effect size quantification")
print(" - Proper date formatting")
# Return figure for inline display
return plt.gcf(),
</code>
<best_practice>
For interrupted time series: Always mark the intervention clearly, show pre/post trends separately, quantify level and slope changes, and include uncertainty bands.
</best_practice>
</example>
<example context="publication_panel_figure" difficulty="advanced">
<description>Multi-panel publication-ready figure with shared aesthetics</description>
<code>
```python
@app.cell
def create_publication_panel_figure():
#Multi-panel figures are standard in publications.
# This example shows how to create a complex figure with multiple
# related plots that share formatting and meet journal standards.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
import pandas as pd
import seaborn as sns
from scipy import stats
import os
# Generate example data
np.random.seed(123)
n = 200
df = pd.DataFrame({
'x': np.random.normal(50, 15, n),
'y': np.random.normal(100, 20, n),
'group': np.random.choice(['Control', 'Treatment'], n),
'covariate': np.random.uniform(0, 100, n)
})
# Add treatment effect
treatment_mask = df['group'] == 'Treatment'
df.loc[treatment_mask, 'y'] += 10 + 0.3 * df.loc[treatment_mask, 'covariate']
# MANDATORY: Set up publication-quality figure
# For Nature/Science: width = 180mm (7.09 inches) for full page
fig = plt.figure(figsize=(7, 8), dpi=300)
# Use GridSpec for complex layout
gs = gridspec.GridSpec(3, 2, height_ratios=[1, 1, 0.8],
width_ratios=[1, 1], hspace=0.3, wspace=0.3)
# Colorblind-safe colors
colors = {'Control': '#0173B2', 'Treatment': '#DE8F05'}
# PANEL A: Distribution comparison
ax_a = fig.add_subplot(gs[0, :])
for group in ['Control', 'Treatment']:
subset = df[df['group'] == group]['y']
# Histogram with KDE
counts, bins, _ = ax_a.hist(subset, bins=20, alpha=0.5,
label=group, color=colors[group],
edgecolor='black', linewidth=0.5)
# Add KDE
kde = stats.gaussian_kde(subset)
x_range = np.linspace(subset.min(), subset.max(), 100)
kde_values = kde(x_range) * len(subset) * (bins[1] - bins[0])
ax_a.plot(x_range, kde_values, color=colors[group],
linewidth=2, alpha=0.8)
# Add mean line
mean_val = subset.mean()
ax_a.axvline(mean_val, color=colors[group], linestyle='--',
linewidth=2, alpha=0.7)
# Add text with stats
ax_a.text(mean_val, ax_a.get_ylim()[1]*0.9,
f'μ={mean_val:.1f}',
ha='center', fontsize=8, color=colors[group])
ax_a.set_xlabel('Outcome (units)', fontweight='bold')
ax_a.set_ylabel('Frequency', fontweight='bold')
ax_a.set_title('A. Distribution by Group', fontweight='bold', loc='left')
ax_a.legend(loc='upper right')
ax_a.grid(True, alpha=0.3)
# Statistical test annotation
t_stat, p_value = stats.ttest_ind(df[df['group'] == 'Control']['y'],
df[df['group'] == 'Treatment']['y'])
ax_a.text(0.02, 0.95, f't-test: p={p_value:.3f}',
transform=ax_a.transAxes, fontsize=9,
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
# PANEL B: Scatter plot with regression
ax_b = fig.add_subplot(gs[1, 0])
for group in ['Control', 'Treatment']:
subset = df[df['group'] == group]
ax_b.scatter(subset['covariate'], subset['y'],
alpha=0.6, s=30, color=colors[group],
edgecolor='black', linewidth=0.5, label=group)
# Add regression line
slope, intercept, r_value, _, _ = stats.linregress(subset['covariate'], subset['y'])
x_line = np.linspace(subset['covariate'].min(), subset['covariate'].max(), 100)
y_line = slope * x_line + intercept
ax_b.plot(x_line, y_line, color=colors[group],
linewidth=2, alpha=0.8)
ax_b.set_xlabel('Covariate (units)', fontweight='bold')
ax_b.set_ylabel('Outcome (units)', fontweight='bold')
ax_b.set_title('B. Relationship with Covariate', fontweight='bold', loc='left')
ax_b.legend(loc='upper left', fontsize=9)
ax_b.grid(True, alpha=0.3)
# PANEL C: Box plot comparison
ax_c = fig.add_subplot(gs[1, 1])
positions = [1, 2]
box_data = [df[df['group'] == 'Control']['y'],
df[df['group'] == 'Treatment']['y']]
bp = ax_c.boxplot(box_data, positions=positions,
widths=0.6, patch_artist=True,
showfliers=True, showmeans=True)
# Color the boxes
for patch, group in zip(bp['boxes'], ['Control', 'Treatment']):
patch.set_facecolor(colors[group])
patch.set_alpha(0.7)
# Customize appearance
for element in ['whiskers', 'fliers', 'means', 'medians', 'caps']:
plt.setp(bp[element], color='black', linewidth=1.5)
ax_c.set_xticks(positions)
ax_c.set_xticklabels(['Control', 'Treatment'])
ax_c.set_ylabel('Outcome (units)', fontweight='bold')
ax_c.set_title('C. Group Comparison', fontweight='bold', loc='left')
ax_c.grid(True, alpha=0.3, axis='y')
# Add sample sizes
for i, (pos, group) in enumerate(zip(positions, ['Control', 'Treatment'])):
n_group = len(df[df['group'] == group])
ax_c.text(pos, ax_c.get_ylim()[0] - 5, f'n={n_group}',
ha='center', fontsize=9)
# PANEL D: Effect size with CI
ax_d = fig.add_subplot(gs[2, :])
# Calculate effect sizes for different quantiles
quantiles = [0.1, 0.25, 0.5, 0.75, 0.9]
effects = []
ci_lower = []
ci_upper = []
for q in quantiles:
control_q = df[df['group'] == 'Control']['y'].quantile(q)
treatment_q = df[df['group'] == 'Treatment']['y'].quantile(q)
effect = treatment_q - control_q
effects.append(effect)
# Bootstrap CI
n_bootstrap = 1000
bootstrap_effects = []
for _ in range(n_bootstrap):
control_sample = df[df['group'] == 'Control']['y'].sample(n=100, replace=True)
treatment_sample = df[df['group'] == 'Treatment']['y'].sample(n=100, replace=True)
bootstrap_effects.append(treatment_sample.quantile(q) - control_sample.quantile(q))
ci_lower.append(np.percentile(bootstrap_effects, 2.5))
ci_upper.append(np.percentile(bootstrap_effects, 97.5))
# Plot quantile treatment effects
ax_d.errorbar(quantiles, effects, yerr=[np.array(effects) - np.array(ci_lower),
np.array(ci_upper) - np.array(effects)],
fmt='o-', markersize=8, linewidth=2, capsize=5,
color='#CC78BC', ecolor='gray', alpha=0.8)
ax_d.axhline(0, color='red', linestyle='--', alpha=0.5, linewidth=1.5)
ax_d.set_xlabel('Quantile', fontweight='bold')
ax_d.set_ylabel('Treatment Effect (units)', fontweight='bold')
ax_d.set_title('D. Quantile Treatment Effects (95% Bootstrap CI)',
fontweight='bold', loc='left')
ax_d.grid(True, alpha=0.3)
# Overall title
fig.suptitle('Comprehensive Treatment Effect Analysis',
fontsize=16, fontweight='bold', y=0.98)
# Add figure caption
caption = ("Figure 1. Multi-panel analysis of treatment effects. " +
"(A) Distribution comparison showing treatment shifts the outcome distribution. " +
"(B) Covariate relationship reveals heterogeneous treatment effects. " +
"(C) Box plot comparison with means (triangles) and medians (lines). " +
"(D) Quantile treatment effects show larger effects at higher quantiles.")
fig.text(0.1, 0.01, caption, fontsize=9, wrap=True,
ha='left', style='italic')
plt.tight_layout(rect=[0, 0.05, 1, 0.96])
# Save in multiple formats
os.makedirs("./images", exist_ok=True)
# For submission (PDF)
plt.savefig("./images/figure1_publication.pdf", bbox_inches="tight", dpi=300)
# For review (PNG)
plt.savefig("./images/figure1_review.png", bbox_inches="tight", dpi=300)
# For presentations
plt.savefig("./images/figure1_presentation.png", bbox_inches="tight", dpi=144)
print("Publication-ready multi-panel figure created:")
print(" - Four complementary panels (A-D)")
print(" - Consistent color scheme throughout")
print(" - Statistical annotations")
print(" - Bootstrap confidence intervals")
print(" - Complete figure caption")
print(" - Saved in PDF (submission) and PNG (review) formats")
# Return figure for inline display
return plt.gcf(),
1---2name: visualization3description: Publication-quality statistical visualization with mandatory best practices. Use when creating plots, charts, figures, or any data visualization. Enforces accessibility, uncertainty display, proper labeling, and statistical accuracy.4---56<skill_content>78<overview>9Visualization is not decoration—it is statistical communication. A well-designed plot reveals patterns that tables cannot, while a poor plot misleads more effectively than wrong numbers. Every visualization is an argument about data, and this skill enforces the standards that make those arguments honest, accessible, and reproducible.1011The difference between amateur and professional visualization is not aesthetics but statistical integrity: showing uncertainty, avoiding perceptual tricks, ensuring accessibility, and documenting what you've done. This skill enforces visualization practices that meet publication standards while preventing common manipulations.12</overview>1314<philosophy>15<core_principle>16"The purpose of visualization is insight, not pictures" - Ben Shneiderman1718FUNDAMENTAL RULES:191. Every estimate needs uncertainty bands202. Every axis needs units213. Every color scheme needs accessibility testing224. Every truncated axis needs justification235. Every plot needs to be reproducible24</core_principle>25</philosophy>2627<mandatory_requirements>2829<requirement priority="critical">30 <name>Complete Axis Labeling</name>31 <description>MUST label all axes with variable names AND units in parentheses</description>32 <rationale>Cleveland (1985) found unlabeled axes were the #1 source of misinterpretation in published figures</rationale>33 <consequence>Readers misinterpret scale, leading to order-of-magnitude errors in interpretation</consequence>34</requirement>3536<requirement priority="critical">37 <name>Uncertainty Visualization</name>38 <description>ALL point estimates MUST show confidence intervals or standard errors</description>39 <rationale>Cumming et al. (2007) demonstrate that showing uncertainty reduces overconfidence in conclusions by 40%</rationale>40 <consequence>False precision, overconfident claims, inability to distinguish signal from noise</consequence>41</requirement>4243<requirement priority="critical">44 <name>Accessibility Compliance</name>45 <description>MUST use colorblind-safe palettes, test with simulator, provide non-color redundancy</description>46 <rationale>8% of men and 0.5% of women have color vision deficiency (Neitz & Neitz, 2011)</rationale>47 <consequence>Excludes ~4% of readers, may violate journal/grant accessibility requirements</consequence>48</requirement>4950<requirement priority="high">51 <name>Resolution and Format Standards</name>52 <description>Minimum 144 DPI for screens, 300 DPI for print, vector format for publication</description>53 <rationale>Journal standards require 300+ DPI; low resolution causes rejection at submission</rationale>54 <consequence>Desk rejection from journals, pixelated figures in presentations</consequence>55</requirement>5657<requirement priority="high">58 <name>Honest Scaling</name>59 <description>Y-axis MUST start at zero for bar charts, ratios, and percentages unless explicitly justified</description>60 <rationale>Huff (1954) "How to Lie with Statistics" - truncated axes are the most common manipulation</rationale>61 <consequence>Exaggerated effects, misleading comparisons, ethical violations</consequence>62</requirement>6364<requirement priority="critical">65 <name>Return Figure Objects for Display</name>66 <description>MUST return figure objects (fig, plt.gcf(), or plt.gca()) as the last expression in the cell. NEVER use plt.show() - it prevents inline display in notebooks and breaks reproducibility</description>67 <rationale>Notebook environments (Jupyter, marimo) automatically display the last expression. plt.show() blocks execution, prevents inline display, and makes figures inaccessible for further manipulation. Returning figure objects enables notebook display, programmatic access, and proper figure management</rationale>68 <consequence>Figures don't display in notebooks, code execution blocked, figures cannot be saved or manipulated programmatically, breaks notebook workflow</consequence>69</requirement>7071</mandatory_requirements>7273<thinking_process>74When creating statistical visualizations:751. Choose plot type based on data structure and question762. Set up proper figure dimensions and resolution773. Plot data with appropriate aesthetics784. Add ALL required labels with units795. Include uncertainty measures (CI, SE, prediction bands)806. Test accessibility (colorblind simulation)817. Add statistical annotations (p-values, R², n)828. Save in multiple formats (PNG for viewing, PDF for publication)839. Return figure object (fig, plt.gcf(), or plt.gca()) as last expression - NEVER use plt.show()8410. Document code for reproducibility85</thinking_process>8687<implementation_pattern>8889<code_template>90```python91# CRITICAL: Publication-quality visualization template9293@app.cell94def create_publication_figure(df, x_var, y_var, group_var=None):95 """96 Every visualization must meet publication standards.97 This template enforces labeling, uncertainty, accessibility, and98 reproducibility requirements that prevent common mistakes.99 """100 import matplotlib.pyplot as plt101 import numpy as np102 import pandas as pd103 import seaborn as sns104 import os105 from matplotlib import cm106107 # MANDATORY: Set publication-quality defaults108 plt.rcParams.update({109 'font.size': 11,110 'font.family': 'sans-serif',111 'axes.labelsize': 12,112 'axes.titlesize': 13,113 'xtick.labelsize': 10,114 'ytick.labelsize': 10,115 'legend.fontsize': 10,116 'figure.titlesize': 14,117 'axes.linewidth': 1.5,118 'axes.grid': True,119 'grid.alpha': 0.3,120 })121122 # MANDATORY: Colorblind-safe palette123 # Source: Wong, B. (2011) Nature Methods 8, 441124 colorblind_colors = [125 '#0173B2', # Blue126 '#DE8F05', # Orange127 '#029E73', # Green128 '#CC78BC', # Light purple129 '#ECE133', # Yellow130 '#56B4E9', # Light blue131 '#F0E442', # Light yellow132 ]133134 # Create figure with specific size for journal column width135 # Single column: 3.5", double column: 7"136 fig, ax = plt.subplots(figsize=(7, 5), dpi=144)137138 # Get data statistics for annotations139 n_obs = len(df)140 x_mean = df[x_var].mean()141 y_mean = df[y_var].mean()142143 if group_var is None:144 # Single group with confidence band145 ax.scatter(df[x_var], df[y_var], alpha=0.6, s=50,146 color=colorblind_colors[0], edgecolor='black', linewidth=0.5)147148 # Add regression line with confidence interval149 from scipy import stats150 slope, intercept, r_value, p_value, std_err = stats.linregress(df[x_var], df[y_var])151152 x_line = np.linspace(df[x_var].min(), df[x_var].max(), 100)153 y_line = slope * x_line + intercept154155 # Calculate confidence interval for regression line156 from scipy.stats import t157 predict_se = std_err * np.sqrt(1/n_obs + (x_line - x_mean)**2 / ((df[x_var] - x_mean)**2).sum())158 margin = t.ppf(0.975, n_obs - 2) * predict_se159160 ax.plot(x_line, y_line, color=colorblind_colors[0], linewidth=2,161 label=f'Fit (R² = {r_value**2:.3f})')162 ax.fill_between(x_line, y_line - margin, y_line + margin,163 alpha=0.2, color=colorblind_colors[0], label='95% CI')164165 # Add statistical annotation166 ax.text(0.05, 0.95, f'n = {n_obs}\np = {p_value:.3f}',167 transform=ax.transAxes, verticalalignment='top',168 bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))169170 else:171 # Multiple groups with distinct colors and markers172 markers = ['o', 's', '^', 'D', 'v', '<', '>']173 groups = df[group_var].unique()174175 for i, group in enumerate(groups):176 subset = df[df[group_var] == group]177 color = colorblind_colors[i % len(colorblind_colors)]178 marker = markers[i % len(markers)]179180 # Plot with confidence intervals181 x_vals = subset.groupby(x_var)[y_var].mean()182 x_sems = subset.groupby(x_var)[y_var].sem()183184 ax.errorbar(x_vals.index, x_vals.values, yerr=1.96*x_sems.values,185 marker=marker, markersize=8, linewidth=2, capsize=5,186 color=color, label=f'{group} (n={len(subset)})',187 alpha=0.8)188189 # MANDATORY: Complete axis labeling with units190 # Extract units from variable names if formatted as "variable (unit)"191 x_label = x_var if '(' in x_var else f"{x_var} (units)"192 y_label = y_var if '(' in y_var else f"{y_var} (units)"193194 ax.set_xlabel(x_label, fontweight='bold')195 ax.set_ylabel(y_label, fontweight='bold')196197 # MANDATORY: Informative title198 if group_var:199 ax.set_title(f'{y_var} vs {x_var} by {group_var}\n(Mean ± 95% CI)',200 fontweight='bold', pad=20)201 else:202 ax.set_title(f'{y_var} vs {x_var}\n(with 95% Confidence Band)',203 fontweight='bold', pad=20)204205 # MANDATORY: Grid for readability206 ax.grid(True, alpha=0.3, linestyle='--')207208 # MANDATORY: Legend with sample sizes209 if group_var or True: # Always show legend for clarity210 ax.legend(loc='best', frameon=True, fancybox=True, shadow=True)211212 # Check if y-axis should start at zero (for ratios, percentages, counts)213 if any(keyword in y_var.lower() for keyword in ['percent', 'ratio', 'proportion', 'count']):214 y_min, y_max = ax.get_ylim()215 if y_min > 0:216 ax.set_ylim(bottom=0, top=y_max * 1.1)217 ax.annotate('Note: Y-axis starts at zero', xy=(0.5, -0.15),218 xycoords='axes fraction', ha='center', fontsize=9, style='italic')219220 # Add data source and timestamp for reproducibility221 fig.text(0.99, 0.01, f'Data: {n_obs} observations | Generated: {pd.Timestamp.now().strftime("%Y-%m-%d")}',222 ha='right', fontsize=8, style='italic', alpha=0.7)223224 # Tight layout to prevent label cutoff225 plt.tight_layout()226227 # MANDATORY: Save in multiple formats228 os.makedirs("./images", exist_ok=True)229230 # Screen viewing (PNG at 144 DPI)231 plt.savefig("./images/figure_screen.png", dpi=144, bbox_inches="tight")232233 # Publication (PDF vector format)234 plt.savefig("./images/figure_publication.pdf", bbox_inches="tight")235236 # High-res for presentations (PNG at 300 DPI)237 plt.savefig("./images/figure_highres.png", dpi=300, bbox_inches="tight")238239 print("Figure saved in three formats:")240 print(" ./images/figure_screen.png (144 DPI for screens)")241 print(" ./images/figure_publication.pdf (vector for journals)")242 print(" ./images/figure_highres.png (300 DPI for presentations)")243244 # CRITICAL: Return figure for inline display in notebook245 # Do NOT call plt.show() - it prevents inline display and blocks execution246 # Do NOT call plt.close() - this would prevent display247 # MUST return figure object (fig, plt.gcf(), or plt.gca()) as last expression248 return fig,249```250</code_template>251252</implementation_pattern>253254<examples>255256<example context="treatment_effects" difficulty="basic">257<description>Visualizing treatment effects with proper uncertainty bands</description>258<code>259```python260@app.cell261def plot_treatment_effects():262 #Treatment effects must show uncertainty to enable263 # proper inference. This example shows the gold standard for264 # presenting experimental results with multiple treatments.265266 import matplotlib.pyplot as plt267 import numpy as np268 import pandas as pd269 import os270271 # Simulated treatment effects from an experiment272 treatments = ['Control', 'Treatment A', 'Treatment B', 'Treatment C']273 effects = [0, 2.3, 3.7, 1.8]274 std_errors = [0.5, 0.6, 0.8, 0.7]275 sample_sizes = [102, 98, 95, 101]276 p_values = [1.000, 0.001, 0.0001, 0.042]277278 # MANDATORY: Colorblind-safe palette279 colors = ['#808080', '#0173B2', '#029E73', '#DE8F05']280281 fig, ax = plt.subplots(figsize=(10, 6), dpi=144)282283 # Calculate 95% confidence intervals284 ci_lower = [e - 1.96*se for e, se in zip(effects, std_errors)]285 ci_upper = [e + 1.96*se for e, se in zip(effects, std_errors)]286287 # Create coefficient plot (forest plot style)288 y_positions = range(len(treatments))289290 for i, (treatment, effect, lower, upper, n, p, color) in enumerate(291 zip(treatments, effects, ci_lower, ci_upper, sample_sizes, p_values, colors)292 ):293 # Plot point estimate294 ax.scatter(effect, i, s=150, color=color, zorder=3,295 edgecolors='black', linewidth=1.5)296297 # Plot confidence interval298 ax.plot([lower, upper], [i, i], linewidth=3, color=color, alpha=0.7)299300 # Add caps to CI301 cap_width = 0.05302 ax.plot([lower, lower], [i-cap_width, i+cap_width], linewidth=2, color=color)303 ax.plot([upper, upper], [i-cap_width, i+cap_width], linewidth=2, color=color)304305 # Add text annotations with sample size and p-value306 significance = '***' if p < 0.001 else '**' if p < 0.01 else '*' if p < 0.05 else 'ns'307 ax.text(upper + 0.2, i, f'n={n}, p={p:.3f} {significance}',308 verticalalignment='center', fontsize=9)309310 # MANDATORY: Reference line at zero311 ax.axvline(0, color='red', linestyle='--', alpha=0.5, linewidth=1.5,312 label='No effect')313314 # Shade region of practical significance (example: ±0.5)315 ax.axvspan(-0.5, 0.5, alpha=0.1, color='gray',316 label='Region of practical equivalence')317318 # MANDATORY: Complete labeling319 ax.set_yticks(y_positions)320 ax.set_yticklabels(treatments)321 ax.set_xlabel('Treatment Effect (units of outcome)', fontweight='bold', fontsize=12)322 ax.set_title('Treatment Effects with 95% Confidence Intervals\nRelative to Control Group',323 fontweight='bold', fontsize=14)324325 # Add grid for easier reading326 ax.grid(True, axis='x', alpha=0.3, linestyle='--')327328 # Legend329 ax.legend(loc='upper right', frameon=True)330331 # Add interpretation guide332 fig.text(0.12, 0.02,333 'Interpretation: Points show estimates, bars show 95% CI. ' +334 'Effects excluding zero are statistically significant at α=0.05.',335 fontsize=9, style='italic', wrap=True)336337 # Statistical significance legend338 fig.text(0.88, 0.02,339 '*** p<0.001, ** p<0.01, * p<0.05, ns: not significant',340 ha='right', fontsize=8, style='italic')341342 plt.tight_layout(rect=[0, 0.05, 1, 1])343344 # Save345 os.makedirs("./images", exist_ok=True)346 plt.savefig("./images/treatment_effects.png", dpi=144, bbox_inches="tight")347 plt.savefig("./images/treatment_effects.pdf", bbox_inches="tight")348349 print("Treatment effect plot created with:")350 print(" - 95% confidence intervals")351 print(" - Sample sizes and p-values")352 print(" - Colorblind-safe colors")353 print(" - Reference line at zero")354 print(" - Region of practical equivalence")355356 # Return figure for inline display357 return plt.gcf(),358```359</code>360<lesson>361Forest plots are the gold standard for showing treatment effects. Always include: point estimates, confidence intervals, sample sizes, p-values, and a reference line at zero.362</lesson>363</example>364365<example context="time_series_with_events" difficulty="intermediate">366<description>Time series with intervention points and uncertainty bands</description>367<code>368```python369@app.cell370def plot_time_series_with_intervention():371 #Time series plots must clearly show when interventions372 # occurred and quantify uncertainty around trends. This example shows373 # best practices for interrupted time series visualization.374375 import matplotlib.pyplot as plt376 import numpy as np377 import pandas as pd378 from scipy import stats379 import os380381 # Generate example time series data382 np.random.seed(42)383 dates = pd.date_range('2020-01-01', '2023-12-31', freq='M')384 n_points = len(dates)385386 # Pre-intervention trend387 pre_intervention = 36 # Month 36 is intervention388 trend_pre = 100 + 2 * np.arange(pre_intervention)389 noise_pre = np.random.normal(0, 10, pre_intervention)390391 # Post-intervention (level shift + trend change)392 level_shift = 20393 trend_change = -1.5394 trend_post = (100 + 2 * pre_intervention + level_shift +395 trend_change * np.arange(n_points - pre_intervention))396 noise_post = np.random.normal(0, 12, n_points - pre_intervention)397398 # Combine399 values = np.concatenate([trend_pre + noise_pre, trend_post + noise_post])400401 df = pd.DataFrame({402 'date': dates,403 'value': values,404 'period': ['Pre' if i < pre_intervention else 'Post' for i in range(n_points)]405 })406407 # Calculate rolling mean and std for uncertainty band408 df['rolling_mean'] = df['value'].rolling(window=3, center=True).mean()409 df['rolling_std'] = df['value'].rolling(window=3, center=True).std()410411 # VISUALIZATION412 fig, ax = plt.subplots(figsize=(14, 7), dpi=144)413414 # Different colors for pre/post periods415 pre_color = '#0173B2' # Blue416 post_color = '#DE8F05' # Orange417418 # Plot pre-intervention419 pre_data = df[df['period'] == 'Pre']420 ax.plot(pre_data['date'], pre_data['value'],421 marker='o', markersize=4, linewidth=1.5,422 color=pre_color, label='Pre-intervention', alpha=0.8)423424 # Plot post-intervention425 post_data = df[df['period'] == 'Post']426 ax.plot(post_data['date'], post_data['value'],427 marker='s', markersize=4, linewidth=1.5,428 color=post_color, label='Post-intervention', alpha=0.8)429430 # Add uncertainty bands (±1.96 SE)431 ax.fill_between(df['date'],432 df['rolling_mean'] - 1.96*df['rolling_std'].fillna(0),433 df['rolling_mean'] + 1.96*df['rolling_std'].fillna(0),434 alpha=0.2, color='gray', label='95% CI (rolling)')435436 # MANDATORY: Mark intervention point437 intervention_date = dates[pre_intervention]438 ax.axvline(intervention_date, color='red', linestyle='--', linewidth=2,439 label='Intervention', alpha=0.7)440441 # Add shaded region for intervention period442 ax.axvspan(intervention_date, dates[-1], alpha=0.1, color='yellow')443444 # Fit trend lines for each period445 from sklearn.linear_model import LinearRegression446447 # Pre-intervention trend448 X_pre = np.arange(len(pre_data)).reshape(-1, 1)449 model_pre = LinearRegression().fit(X_pre, pre_data['value'])450 trend_line_pre = model_pre.predict(X_pre)451 ax.plot(pre_data['date'], trend_line_pre, '--',452 color=pre_color, linewidth=2, alpha=0.5)453454 # Post-intervention trend455 X_post = np.arange(len(post_data)).reshape(-1, 1)456 model_post = LinearRegression().fit(X_post, post_data['value'])457 trend_line_post = model_post.predict(X_post)458 ax.plot(post_data['date'], trend_line_post, '--',459 color=post_color, linewidth=2, alpha=0.5)460461 # Add annotations with effect sizes462 pre_slope = model_pre.coef_[0]463 post_slope = model_post.coef_[0]464 level_change = trend_line_post[0] - trend_line_pre[-1]465466 # Text box with results467 textstr = f'Pre-trend: {pre_slope:.2f}/month\n'468 textstr += f'Post-trend: {post_slope:.2f}/month\n'469 textstr += f'Level change: {level_change:.1f} units\n'470 textstr += f'Trend change: {post_slope - pre_slope:.2f}/month'471472 props = dict(boxstyle='round', facecolor='wheat', alpha=0.8)473 ax.text(0.02, 0.98, textstr, transform=ax.transAxes, fontsize=10,474 verticalalignment='top', bbox=props)475476 # MANDATORY: Complete labeling477 ax.set_xlabel('Date', fontweight='bold', fontsize=12)478 ax.set_ylabel('Outcome Measure (units)', fontweight='bold', fontsize=12)479 ax.set_title('Interrupted Time Series Analysis\nWith Intervention Effect Quantification',480 fontweight='bold', fontsize=14)481482 # Format x-axis dates483 import matplotlib.dates as mdates484 ax.xaxis.set_major_locator(mdates.YearLocator())485 ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))486 ax.xaxis.set_minor_locator(mdates.MonthLocator([1, 4, 7, 10]))487488 # Grid and legend489 ax.grid(True, alpha=0.3, linestyle='--')490 ax.legend(loc='upper left', frameon=True)491492 # Add note about statistical testing493 fig.text(0.5, 0.01,494 'Note: Formal interrupted time series analysis required for causal inference',495 ha='center', fontsize=9, style='italic')496497 plt.tight_layout()498499 # Save500 os.makedirs("./images", exist_ok=True)501 plt.savefig("./images/time_series_intervention.png", dpi=144, bbox_inches="tight")502 plt.savefig("./images/time_series_intervention.pdf", bbox_inches="tight")503504 print("Time series plot created with:")505 print(" - Clear intervention marking")506 print(" - Pre/post trend lines")507 print(" - Uncertainty bands")508 print(" - Effect size quantification")509 print(" - Proper date formatting")510511 # Return figure for inline display512 return plt.gcf(),513```514</code>515<best_practice>516For interrupted time series: Always mark the intervention clearly, show pre/post trends separately, quantify level and slope changes, and include uncertainty bands.517</best_practice>518</example>519520<example context="publication_panel_figure" difficulty="advanced">521<description>Multi-panel publication-ready figure with shared aesthetics</description>522<code>523```python524@app.cell525def create_publication_panel_figure():526 #Multi-panel figures are standard in publications.527 # This example shows how to create a complex figure with multiple528 # related plots that share formatting and meet journal standards.529530 import matplotlib.pyplot as plt531 import matplotlib.gridspec as gridspec532 import numpy as np533 import pandas as pd534 import seaborn as sns535 from scipy import stats536 import os537538 # Generate example data539 np.random.seed(123)540 n = 200541 df = pd.DataFrame({542 'x': np.random.normal(50, 15, n),543 'y': np.random.normal(100, 20, n),544 'group': np.random.choice(['Control', 'Treatment'], n),545 'covariate': np.random.uniform(0, 100, n)546 })547548 # Add treatment effect549 treatment_mask = df['group'] == 'Treatment'550 df.loc[treatment_mask, 'y'] += 10 + 0.3 * df.loc[treatment_mask, 'covariate']551552 # MANDATORY: Set up publication-quality figure553 # For Nature/Science: width = 180mm (7.09 inches) for full page554 fig = plt.figure(figsize=(7, 8), dpi=300)555556 # Use GridSpec for complex layout557 gs = gridspec.GridSpec(3, 2, height_ratios=[1, 1, 0.8],558 width_ratios=[1, 1], hspace=0.3, wspace=0.3)559560 # Colorblind-safe colors561 colors = {'Control': '#0173B2', 'Treatment': '#DE8F05'}562563 # PANEL A: Distribution comparison564 ax_a = fig.add_subplot(gs[0, :])565566 for group in ['Control', 'Treatment']:567 subset = df[df['group'] == group]['y']568569 # Histogram with KDE570 counts, bins, _ = ax_a.hist(subset, bins=20, alpha=0.5,571 label=group, color=colors[group],572 edgecolor='black', linewidth=0.5)573574 # Add KDE575 kde = stats.gaussian_kde(subset)576 x_range = np.linspace(subset.min(), subset.max(), 100)577 kde_values = kde(x_range) * len(subset) * (bins[1] - bins[0])578 ax_a.plot(x_range, kde_values, color=colors[group],579 linewidth=2, alpha=0.8)580581 # Add mean line582 mean_val = subset.mean()583 ax_a.axvline(mean_val, color=colors[group], linestyle='--',584 linewidth=2, alpha=0.7)585586 # Add text with stats587 ax_a.text(mean_val, ax_a.get_ylim()[1]*0.9,588 f'μ={mean_val:.1f}',589 ha='center', fontsize=8, color=colors[group])590591 ax_a.set_xlabel('Outcome (units)', fontweight='bold')592 ax_a.set_ylabel('Frequency', fontweight='bold')593 ax_a.set_title('A. Distribution by Group', fontweight='bold', loc='left')594 ax_a.legend(loc='upper right')595 ax_a.grid(True, alpha=0.3)596597 # Statistical test annotation598 t_stat, p_value = stats.ttest_ind(df[df['group'] == 'Control']['y'],599 df[df['group'] == 'Treatment']['y'])600 ax_a.text(0.02, 0.95, f't-test: p={p_value:.3f}',601 transform=ax_a.transAxes, fontsize=9,602 bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))603604 # PANEL B: Scatter plot with regression605 ax_b = fig.add_subplot(gs[1, 0])606607 for group in ['Control', 'Treatment']:608 subset = df[df['group'] == group]609 ax_b.scatter(subset['covariate'], subset['y'],610 alpha=0.6, s=30, color=colors[group],611 edgecolor='black', linewidth=0.5, label=group)612613 # Add regression line614 slope, intercept, r_value, _, _ = stats.linregress(subset['covariate'], subset['y'])615 x_line = np.linspace(subset['covariate'].min(), subset['covariate'].max(), 100)616 y_line = slope * x_line + intercept617 ax_b.plot(x_line, y_line, color=colors[group],618 linewidth=2, alpha=0.8)619620 ax_b.set_xlabel('Covariate (units)', fontweight='bold')621 ax_b.set_ylabel('Outcome (units)', fontweight='bold')622 ax_b.set_title('B. Relationship with Covariate', fontweight='bold', loc='left')623 ax_b.legend(loc='upper left', fontsize=9)624 ax_b.grid(True, alpha=0.3)625626 # PANEL C: Box plot comparison627 ax_c = fig.add_subplot(gs[1, 1])628629 positions = [1, 2]630 box_data = [df[df['group'] == 'Control']['y'],631 df[df['group'] == 'Treatment']['y']]632633 bp = ax_c.boxplot(box_data, positions=positions,634 widths=0.6, patch_artist=True,635 showfliers=True, showmeans=True)636637 # Color the boxes638 for patch, group in zip(bp['boxes'], ['Control', 'Treatment']):639 patch.set_facecolor(colors[group])640 patch.set_alpha(0.7)641642 # Customize appearance643 for element in ['whiskers', 'fliers', 'means', 'medians', 'caps']:644 plt.setp(bp[element], color='black', linewidth=1.5)645646 ax_c.set_xticks(positions)647 ax_c.set_xticklabels(['Control', 'Treatment'])648 ax_c.set_ylabel('Outcome (units)', fontweight='bold')649 ax_c.set_title('C. Group Comparison', fontweight='bold', loc='left')650 ax_c.grid(True, alpha=0.3, axis='y')651652 # Add sample sizes653 for i, (pos, group) in enumerate(zip(positions, ['Control', 'Treatment'])):654 n_group = len(df[df['group'] == group])655 ax_c.text(pos, ax_c.get_ylim()[0] - 5, f'n={n_group}',656 ha='center', fontsize=9)657658 # PANEL D: Effect size with CI659 ax_d = fig.add_subplot(gs[2, :])660661 # Calculate effect sizes for different quantiles662 quantiles = [0.1, 0.25, 0.5, 0.75, 0.9]663 effects = []664 ci_lower = []665 ci_upper = []666667 for q in quantiles:668 control_q = df[df['group'] == 'Control']['y'].quantile(q)669 treatment_q = df[df['group'] == 'Treatment']['y'].quantile(q)670 effect = treatment_q - control_q671 effects.append(effect)672673 # Bootstrap CI674 n_bootstrap = 1000675 bootstrap_effects = []676 for _ in range(n_bootstrap):677 control_sample = df[df['group'] == 'Control']['y'].sample(n=100, replace=True)678 treatment_sample = df[df['group'] == 'Treatment']['y'].sample(n=100, replace=True)679 bootstrap_effects.append(treatment_sample.quantile(q) - control_sample.quantile(q))680681 ci_lower.append(np.percentile(bootstrap_effects, 2.5))682 ci_upper.append(np.percentile(bootstrap_effects, 97.5))683684 # Plot quantile treatment effects685 ax_d.errorbar(quantiles, effects, yerr=[np.array(effects) - np.array(ci_lower),686 np.array(ci_upper) - np.array(effects)],687 fmt='o-', markersize=8, linewidth=2, capsize=5,688 color='#CC78BC', ecolor='gray', alpha=0.8)689690 ax_d.axhline(0, color='red', linestyle='--', alpha=0.5, linewidth=1.5)691 ax_d.set_xlabel('Quantile', fontweight='bold')692 ax_d.set_ylabel('Treatment Effect (units)', fontweight='bold')693 ax_d.set_title('D. Quantile Treatment Effects (95% Bootstrap CI)',694 fontweight='bold', loc='left')695 ax_d.grid(True, alpha=0.3)696697 # Overall title698 fig.suptitle('Comprehensive Treatment Effect Analysis',699 fontsize=16, fontweight='bold', y=0.98)700701 # Add figure caption702 caption = ("Figure 1. Multi-panel analysis of treatment effects. " +703 "(A) Distribution comparison showing treatment shifts the outcome distribution. " +704 "(B) Covariate relationship reveals heterogeneous treatment effects. " +705 "(C) Box plot comparison with means (triangles) and medians (lines). " +706 "(D) Quantile treatment effects show larger effects at higher quantiles.")707708 fig.text(0.1, 0.01, caption, fontsize=9, wrap=True,709 ha='left', style='italic')710711 plt.tight_layout(rect=[0, 0.05, 1, 0.96])712713 # Save in multiple formats714 os.makedirs("./images", exist_ok=True)715716 # For submission (PDF)717 plt.savefig("./images/figure1_publication.pdf", bbox_inches="tight", dpi=300)718719 # For review (PNG)720 plt.savefig("./images/figure1_review.png", bbox_inches="tight", dpi=300)721722 # For presentations723 plt.savefig("./images/figure1_presentation.png", bbox_inches="tight", dpi=144)724725 print("Publication-ready multi-panel figure created:")726 print(" - Four complementary panels (A-D)")727 print(" - Consistent color scheme throughout")728 print(" - Statistical annotations")729 print(" - Bootstrap confidence intervals")730 print(" - Complete figure caption")731732 print(" - Saved in PDF (submission) and PNG (review) formats")733734 # Return figure for inline display735 return plt.gcf(),736```737</code>738<power_user_tip>739For multi-panel figures: Use GridSpec for precise control, maintain consistent aesthetics across panels, label panels with letters (A, B, C...), and include a comprehensive caption that explains each panel.740</power_user_tip>741</example>742743</examples>744745<common_mistakes>746747<mistake severity="critical">748 <what>Missing uncertainty visualization (no error bars/CI)</what>749 <consequence>Readers cannot distinguish signal from noise, overconfident interpretations</consequence>750 <prevention>ALWAYS add error bars, confidence bands, or prediction intervals to estimates</prevention>751</mistake>752753<mistake severity="critical">754 <what>Unlabeled or partially labeled axes</what>755 <consequence>Misinterpretation of scale, units, or variables being plotted</consequence>756 <prevention>Label BOTH axes with variable name AND units in parentheses</prevention>757</mistake>758759<mistake severity="critical">760 <what>Using red-green color schemes</what>761 <consequence>Invisible to 8% of male readers with color blindness</consequence>762 <prevention>Use colorblind-safe palettes (blue-orange, purple-green), test with simulator</prevention>763</mistake>764765<mistake severity="high">766 <what>Truncating y-axis to exaggerate differences</what>767 <consequence>Misleading visual impression, ethical violation, desk rejection</consequence>768 <prevention>Start bar charts at zero, clearly mark and justify any truncation</prevention>769</mistake>770771<mistake severity="high">772 <what>Low resolution figures (< 144 DPI)</what>773 <consequence>Journal desk rejection, pixelated presentations</consequence>774 <prevention>Save at 144 DPI minimum for screens, 300 DPI for print, vector for publication</prevention>775</mistake>776777<mistake severity="critical">778 <what>Using plt.show() instead of returning figure objects</what>779 <consequence>Figures don't display in notebooks, code execution blocked, figures cannot be saved or manipulated programmatically, breaks notebook workflow</consequence>780 <prevention>ALWAYS return figure object (fig, plt.gcf(), or plt.gca()) as the last expression. Notebooks automatically display the last expression. Never use plt.show()</prevention>781</mistake>782783<mistake severity="medium">784 <what>Overcrowded plots with too many series</what>785 <consequence>Impossible to distinguish patterns, cognitive overload</consequence>786 <prevention>Limit to 5-7 series maximum, use facets or multiple panels for more</prevention>787</mistake>788789</common_mistakes>790791<interpretation_guide>792793<choosing_plot_type>794**Distribution**: Histogram + KDE, box plot, violin plot795**Comparison**: Grouped bar chart, dot plot with CI, paired slopes796**Relationship**: Scatter with regression line, hexbin for large n797**Time series**: Line plot with markers, area chart for cumulative798**Proportions**: Stacked bar (NOT pie charts)799**Uncertainty**: Forest plot, coefficient plot, funnel plot800</choosing_plot_type>801802<accessibility_checklist>803□ Colorblind-safe palette used804□ Sufficient contrast (WCAG AA standard)805□ Patterns/shapes redundant with color806□ Font size ≥ 10pt807□ Alt text provided for screen readers808□ Grayscale-readable809</accessibility_checklist>810811<journal_requirements>812**Nature/Science**: 180mm width, 300 DPI minimum, PDF preferred813**PLOS**: 6.83" width, vector format, CC-BY license notation814**Economics journals**: Often require EPS format, Times font815**Medical journals**: CONSORT flow diagram for RCTs required816</journal_requirements>817818<honest_limitations>819- Visualization can mislead even with best practices820- Some patterns only visible in tables (exact values)821- Color alone insufficient for many distinctions822- Static plots miss temporal dynamics823- 2D projection loses multivariate relationships824</honest_limitations>825826</interpretation_guide>827828<references>829<paper>Cleveland, W.S. (1985). The Elements of Graphing Data. Foundational principles of statistical graphics.</paper>830<paper>Tufte, E.R. (2001). The Visual Display of Quantitative Information. Classic text on data visualization.</paper>831<paper>Cumming, G., Fidler, F., & Vaux, D.L. (2007). "Error bars in experimental biology." Journal of Cell Biology. Proper uncertainty visualization.</paper>832<paper>Wong, B. (2011). "Points of view: Color blindness." Nature Methods 8(6): 441. Colorblind-safe palette design.</paper>833<paper>Weissgerber, T.L., et al. (2015). "Beyond bar and line graphs." PLOS Biology. Problems with bar charts for continuous data.</paper>834<paper>Huff, D. (1954). How to Lie with Statistics. Common visualization manipulations.</paper>835</references>836837</skill_content>