Install
$ agentstack add skill-sshtomar-claude-code-skills-social-science-visualization ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Visualization 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.
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.
"The purpose of visualization is insight, not pictures" - Ben Shneiderman
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
Complete Axis Labeling MUST label all axes with variable names AND units in parentheses Cleveland (1985) found unlabeled axes were the #1 source of misinterpretation in published figures Readers misinterpret scale, leading to order-of-magnitude errors in interpretation
Uncertainty Visualization ALL point estimates MUST show confidence intervals or standard errors Cumming et al. (2007) demonstrate that showing uncertainty reduces overconfidence in conclusions by 40% False precision, overconfident claims, inability to distinguish signal from noise
Accessibility Compliance MUST use colorblind-safe palettes, test with simulator, provide non-color redundancy 8% of men and 0.5% of women have color vision deficiency (Neitz & Neitz, 2011) Excludes ~4% of readers, may violate journal/grant accessibility requirements
Resolution and Format Standards Minimum 144 DPI for screens, 300 DPI for print, vector format for publication Journal standards require 300+ DPI; low resolution causes rejection at submission Desk rejection from journals, pixelated figures in presentations
Honest Scaling Y-axis MUST start at zero for bar charts, ratios, and percentages unless explicitly justified Huff (1954) "How to Lie with Statistics" - truncated axes are the most common manipulation Exaggerated effects, misleading comparisons, ethical violations
Return Figure Objects for Display 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 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 Figures don't display in notebooks, code execution blocked, figures cannot be saved or manipulated programmatically, breaks notebook workflow
When creating statistical visualizations:
- Choose plot type based on data structure and question
- Set up proper figure dimensions and resolution
- Plot data with appropriate aesthetics
- Add ALL required labels with units
- Include uncertainty measures (CI, SE, prediction bands)
- Test accessibility (colorblind simulation)
- Add statistical annotations (p-values, R², n)
- Save in multiple formats (PNG for viewing, PDF for publication)
- Return figure object (fig, plt.gcf(), or plt.gca()) as last expression - NEVER use plt.show()
- Document code for reproducibility
# CRITICAL: Publication-quality visualization template
@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,
Visualizing treatment effects with proper uncertainty bands
@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
Forest 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.
Time series with intervention points and uncertainty bands
```python
@app.cell
def plot_time_series_with_intervention():
#Time series plots must clearly show when interventions
# occurred and quantify uncertainty around trends. This example shows
# best practices for interrupted time series visualization.
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
For interrupted time series: Always mark the intervention clearly, show pre/post trends separately, quantify level and slope changes, and include uncertainty bands.
Multi-panel publication-ready figure with shared aesthetics
```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)
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [sshtomar](https://github.com/sshtomar)
- **Source:** [sshtomar/claude-code-skills-social-science](https://github.com/sshtomar/claude-code-skills-social-science)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.