Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Statistical visualization on matplotlib with native pandas support. Auto aggregation, CIs, grouping for distributions (histplot, kdeplot), categorical (boxplot, violinplot), relational (scatterplot, lineplot), regression (regplot, lmplot), matrix (heatmap, clustermap), grids (pairplot, FacetGrid). Use for quick statistical summaries; matplotlib for fine control; plotly for interactive HTML.
.claude/skills/jaechang-hits-seaborn-statistical-plots/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-11 | ✗→✓ | ▲ Improved | 1159% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 494% | 0% |
| case-12 | ✓→✓ | = Same ✓ | 668% | 0% |
| case-13 | ✓→✓ | = Same ✓ | 792% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 374% | 0% |
Seaborn is a Python library for statistical data visualization built on top of matplotlib. It works directly with pandas DataFrames, automatically handles grouping by categorical variables, computes confidence intervals and kernel density estimates, and produces attractive publication-ready figures with minimal configuration. Seaborn separates axes-level functions (embeddable in custom layouts) from figure-level functions (with built-in faceting), enabling both quick exploratory analysis and structured multi-panel figures.
pairplot to survey all pairwise relationships at oncematplotlib directly when you need pixel-level control over figure elements, complex mixed-type layouts, or non-statistical custom plotsplotly when the output must be interactive (hover tooltips, zoom, pan) or embedded in a web applicationseaborn>=0.13, matplotlib, pandas, numpybashpip install "seaborn>=0.13" matplotlib pandas numpy scipy
pythonimport seaborn as sns import matplotlib.pyplot as plt import pandas as pd import numpy as np # Simulate gene expression across conditions rng = np.random.default_rng(42) df = pd.DataFrame({ "gene": ["BRCA1"] * 60 + ["TP53"] * 60, "condition": ["control", "treated"] * 60, "log2_expr": np.concatenate([ rng.normal(5.2, 0.8, 60), rng.normal(6.1, 0.9, 60), ]) }) sns.set_theme(style="ticks", context="notebook") sns.boxplot(data=df, x="gene", y="log2_expr", hue="condition", palette="Set2") plt.ylabel("log2 Expression") plt.title("Gene Expression by Condition") plt.tight_layout() plt.savefig("quickstart_boxplot.png", dpi=150) print("Saved quickstart_boxplot.png")
Visualize univariate distributions and compare them across groups. histplot bins data; kdeplot fits a smooth density estimate; displot is the figure-level wrapper that adds faceting.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(0) n = 200 df = pd.DataFrame({ "log2_tpm": np.concatenate([rng.normal(4.5, 1.1, n), rng.normal(6.0, 1.3, n)]), "sample": ["tumor"] * n + ["normal"] * n, }) fig, axes = plt.subplots(1, 3, figsize=(15, 4)) # Histogram with density normalization and stacked hue groups sns.histplot(data=df, x="log2_tpm", hue="sample", stat="density", multiple="stack", bins=30, ax=axes[0]) axes[0].set_title("Histogram (stacked)") # KDE with fill — bandwidth controlled by bw_adjust sns.kdeplot(data=df, x="log2_tpm", hue="sample", fill=True, bw_adjust=0.8, alpha=0.4, ax=axes[1]) axes[1].set_title("KDE (filled)") # ECDF — useful for comparing cumulative distributions sns.ecdfplot(data=df, x="log2_tpm", hue="sample", ax=axes[2]) axes[2].set_title("ECDF") plt.tight_layout() plt.savefig("distributions.png", dpi=150) print("Saved distributions.png")
python# Bivariate KDE: joint distribution of two continuous variables rng = np.random.default_rng(1) df2 = pd.DataFrame({ "log2_rna": rng.normal(5.5, 1.2, 300), "log2_prot": rng.normal(4.8, 1.0, 300) + 0.6 * rng.normal(5.5, 1.2, 300), }) sns.kdeplot(data=df2, x="log2_rna", y="log2_prot", fill=True, levels=8, thresh=0.05, cmap="Blues") plt.xlabel("log2 RNA (TPM)") plt.ylabel("log2 Protein (iBAQ)") plt.title("RNA–Protein Correlation Density") plt.tight_layout() plt.savefig("bivariate_kde.png", dpi=150) print("Saved bivariate_kde.png")
Compare distributions or aggregated statistics across categorical groups. Axes-level functions (boxplot, violinplot, stripplot, swarmplot, barplot) accept an ax= parameter for embedding in custom layouts.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(2) conditions = ["DMSO", "Drug A 1uM", "Drug A 10uM", "Drug B 1uM", "Drug B 10uM"] df = pd.DataFrame({ "condition": np.repeat(conditions, 30), "viability": np.concatenate([ rng.normal(100, 5, 30), rng.normal(92, 7, 30), rng.normal(65, 10, 30), rng.normal(88, 8, 30), rng.normal(45, 12, 30), ]) }) fig, axes = plt.subplots(1, 3, figsize=(18, 5)) # Box plot — shows quartiles and outliers sns.boxplot(data=df, x="condition", y="viability", palette="husl", width=0.5, ax=axes[0]) axes[0].set_xticklabels(axes[0].get_xticklabels(), rotation=30, ha="right") axes[0].set_title("Box Plot") # Violin — KDE shape + inner quartile lines sns.violinplot(data=df, x="condition", y="viability", inner="quart", palette="muted", ax=axes[1]) axes[1].set_xticklabels(axes[1].get_xticklabels(), rotation=30, ha="right") axes[1].set_title("Violin Plot") # Strip plot overlaid on box — shows all individual points sns.boxplot(data=df, x="condition", y="viability", palette="pastel", width=0.5, ax=axes[2]) sns.stripplot(data=df, x="condition", y="viability", color="black", alpha=0.4, size=3, jitter=True, ax=axes[2]) axes[2].set_xticklabels(axes[2].get_xticklabels(), rotation=30, ha="right") axes[2].set_title("Box + Strip") plt.tight_layout() plt.savefig("categorical.png", dpi=150) print("Saved categorical.png")
python# Bar plot with mean ± 95% CI and individual points (swarm) fig, ax = plt.subplots(figsize=(8, 5)) sns.barplot(data=df, x="condition", y="viability", estimator="mean", errorbar="ci", palette="Set3", ax=ax) sns.swarmplot(data=df, x="condition", y="viability", color="black", size=3, alpha=0.5, ax=ax) ax.set_ylabel("Cell Viability (%)") ax.set_xticklabels(ax.get_xticklabels(), rotation=30, ha="right") plt.tight_layout() plt.savefig("barswarm.png", dpi=150) print("Saved barswarm.png")
Visualize relationships between continuous variables. scatterplot and lineplot are axes-level; relplot is the figure-level wrapper that supports col and row faceting.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(3) n = 150 df = pd.DataFrame({ "molecular_weight": rng.uniform(200, 800, n), "logP": rng.uniform(-2, 6, n), "pIC50": rng.normal(6.5, 1.2, n), "target_class": rng.choice(["kinase", "GPCR", "protease"], n), "pass_lipinski": rng.choice(["yes", "no"], n, p=[0.7, 0.3]), }) # Scatter with hue (categorical color) + size (continuous) + style (marker) sns.scatterplot(data=df, x="molecular_weight", y="pIC50", hue="target_class", size="logP", style="pass_lipinski", sizes=(30, 120), alpha=0.7) plt.xlabel("Molecular Weight (Da)") plt.ylabel("pIC50") plt.title("Compound Bioactivity by Target Class") plt.tight_layout() plt.savefig("relational_scatter.png", dpi=150) print("Saved relational_scatter.png")
python# Line plot with automatic mean aggregation and SD error band across replicates timepoints = [0, 1, 2, 4, 8, 24] groups = ["untreated", "low_dose", "high_dose"] rows = [] for grp, base in zip(groups, [100.0, 95.0, 80.0]): for tp in timepoints: for _ in range(5): # 5 replicates rows.append({"timepoint_h": tp, "group": grp, "confluency": base * np.exp(-0.02 * tp * (1 + rng.normal(0, 0.1)))}) time_df = pd.DataFrame(rows) sns.lineplot(data=time_df, x="timepoint_h", y="confluency", hue="group", style="group", errorbar="sd", markers=True, dashes=False) plt.xlabel("Time (h)") plt.ylabel("Confluency (%)") plt.title("Cell Growth Inhibition (mean ± SD, n=5)") plt.tight_layout() plt.savefig("lineplot.png", dpi=150) print("Saved lineplot.png")
Fit linear (or polynomial/lowess) models and visualize them with confidence bands. regplot is axes-level; lmplot is figure-level with faceting support.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(4) n = 120 tumor_size = rng.uniform(0.5, 6.0, n) survival_months = 40 - 5 * tumor_size + rng.normal(0, 4, n) grade = rng.choice(["low", "high"], n, p=[0.5, 0.5]) df = pd.DataFrame({"tumor_size_cm": tumor_size, "survival_months": survival_months, "grade": grade}) fig, axes = plt.subplots(1, 2, figsize=(13, 5)) # Linear regression with 95% CI band sns.regplot(data=df, x="tumor_size_cm", y="survival_months", ci=95, scatter_kws={"alpha": 0.4, "s": 25}, ax=axes[0]) axes[0].set_title("Linear Regression (95% CI)") # Residuals plot — check for homoscedasticity sns.residplot(data=df, x="tumor_size_cm", y="survival_months", scatter_kws={"alpha": 0.4, "s": 25}, ax=axes[1]) axes[1].axhline(0, color="red", linestyle="--", linewidth=1) axes[1].set_title("Residuals vs Fitted") plt.tight_layout() plt.savefig("regression.png", dpi=150) print("Saved regression.png")
python# lmplot — figure-level: separate regression lines per grade (hue) + facets g = sns.lmplot(data=df, x="tumor_size_cm", y="survival_months", hue="grade", col="grade", ci=95, scatter_kws={"alpha": 0.4}, height=4, aspect=1.1) g.set_axis_labels("Tumor Size (cm)", "Survival (months)") g.set_titles("{col_name} grade") g.savefig("lmplot_faceted.png", dpi=150) print("Saved lmplot_faceted.png")
Visualize rectangular data as color-encoded matrices. heatmap is axes-level; clustermap is figure-level and applies hierarchical clustering to rows and columns.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(5) genes = [f"GENE{i}" for i in range(1, 9)] samples = [f"S{i}" for i in range(1, 7)] # Simulate log2 fold-change matrix (rows=genes, cols=samples) lfc = pd.DataFrame( rng.normal(0, 1.5, size=(8, 6)), index=genes, columns=samples ) # Inject a pattern: first 3 genes up in samples 1-3, down in 4-6 lfc.iloc[:3, :3] += 2.5 lfc.iloc[:3, 3:] -= 2.5 # Correlation heatmap of numeric features df_num = pd.DataFrame( rng.standard_normal((80, 5)), columns=["GeneA", "GeneB", "GeneC", "GeneD", "GeneE"] ) df_num["GeneB"] = df_num["GeneA"] * 0.85 + rng.normal(0, 0.3, 80) corr = df_num.corr() fig, axes = plt.subplots(1, 2, figsize=(14, 5)) sns.heatmap(corr, annot=True, fmt=".2f", cmap="coolwarm", center=0, square=True, linewidths=0.5, ax=axes[0]) axes[0].set_title("Pearson Correlation Heatmap") sns.heatmap(lfc, cmap="RdBu_r", center=0, annot=True, fmt=".1f", linewidths=0.3, cbar_kws={"label": "log2FC"}, ax=axes[1]) axes[1].set_title("log2 Fold Change Matrix") plt.tight_layout() plt.savefig("heatmaps.png", dpi=150) print("Saved heatmaps.png")
python# Clustermap with hierarchical clustering and row/column color annotations rng = np.random.default_rng(6) n_genes, n_samples = 30, 16 expr = pd.DataFrame( rng.lognormal(mean=2.0, sigma=1.2, size=(n_genes, n_samples)), index=[f"GENE{i:03d}" for i in range(n_genes)], columns=[f"{'T' if i < 8 else 'N'}{i:02d}" for i in range(n_samples)] ) # Column annotation colors (tumor vs normal) col_colors = ["#D32F2F" if c.startswith("T") else "#1976D2" for c in expr.columns] g = sns.clustermap( np.log2(expr + 1), cmap="viridis", standard_scale=0, # z-score across rows (genes) method="ward", metric="euclidean", col_colors=col_colors, figsize=(12, 10), linewidths=0, cbar_pos=(0.02, 0.8, 0.03, 0.15), cbar_kws={"label": "Row z-score"}, ) g.ax_heatmap.set_xlabel("Sample") g.ax_heatmap.set_ylabel("Gene") plt.savefig("clustermap.png", dpi=150, bbox_inches="tight") print("Saved clustermap.png")
Survey all pairwise relationships with pairplot or display a bivariate distribution with marginals using jointplot. For fully custom grid layouts, use FacetGrid directly.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(7) n = 60 df = pd.DataFrame({ "cell_area": rng.normal(350, 60, n * 3), "nucleus_area": rng.normal(90, 15, n * 3), "mean_intensity": rng.exponential(500, n * 3), "aspect_ratio": np.abs(rng.normal(1.3, 0.3, n * 3)), "cell_type": (["HeLa"] * n + ["MCF7"] * n + ["A549"] * n), }) # Pairplot — matrix of pairwise scatter + KDE on diagonal g = sns.pairplot(df, hue="cell_type", corner=True, diag_kind="kde", plot_kws={"alpha": 0.5, "s": 20}) g.savefig("pairplot.png", dpi=150) print("Saved pairplot.png")
python# Jointplot — bivariate KDE with marginal histograms g = sns.jointplot(data=df, x="cell_area", y="nucleus_area", hue="cell_type", kind="scatter", marginal_kws={"fill": True, "alpha": 0.3}) g.set_axis_labels("Cell Area (µm²)", "Nucleus Area (µm²)") g.savefig("jointplot.png", dpi=150) print("Saved jointplot.png")
python# FacetGrid — custom layout: KDE of mean_intensity per cell type g = sns.FacetGrid(df, col="cell_type", height=3.5, aspect=1.1, sharey=False) g.map(sns.histplot, "mean_intensity", bins=20, kde=True, color="steelblue") g.set_axis_labels("Mean Intensity (AU)", "Count") g.set_titles("{col_name}") g.tight_layout() g.savefig("facetgrid_intensity.png", dpi=150) print("Saved facetgrid_intensity.png")
Seaborn has two tiers of functions with different return types and composability:
| Feature | Axes-Level | Figure-Level | |---------|-----------|--------------| | Examples | scatterplot, histplot, boxplot, heatmap, regplot | relplot, displot, catplot, lmplot | | Returns | matplotlib.axes.Axes | FacetGrid / JointGrid / PairGrid | | Faceting | Manual (create subplots yourself) | Built-in (col=, row= params) | | Sizing | figsize= on parent figure | height= + aspect= per facet panel | | Placement | ax= parameter | Cannot be placed in an existing axes | | Saving | plt.savefig(...) | g.savefig(...) | | Use when | Combining different plot types in one figure | Quick multi-panel exploratory views |
python# Axes-level: place in a pre-allocated subplot grid fig, axes = plt.subplots(1, 2, figsize=(12, 5)) sns.violinplot(data=df, x="cell_type", y="cell_area", ax=axes[0]) sns.scatterplot(data=df, x="cell_area", y="nucleus_area", hue="cell_type", ax=axes[1])
Seaborn semantic mappings (hue, size, style) require long-form (tidy) data where each variable is a column and each observation is a row. Some functions (heatmap, clustermap, lineplot) also accept wide-form.
python# Wide-form: unsuitable for hue/style mappings # sample_A sample_B sample_C # 0 5.1 6.2 4.8 # Long-form (preferred): melt wide → long wide = pd.DataFrame({"sampleA": [5.1, 4.3], "sampleB": [6.2, 5.9]}) long = wide.melt(var_name="sample", value_name="log2_expr") # → columns: sample, log2_expr
Goal: Visualize log2 fold-change vs -log10 p-value (volcano-style) with significance annotations, colored by regulation status, and labeled top hits.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(42) n = 500 lfc = rng.normal(0, 1.5, n) pvals = 10 ** (-rng.exponential(1.5, n)) # skewed toward low significance pvals = np.clip(pvals, 1e-20, 1.0) genes = [f"GENE{i:04d}" for i in range(n)] df_de = pd.DataFrame({"gene": genes, "log2fc": lfc, "pvalue": pvals}) df_de["neg_log10_p"] = -np.log10(df_de["pvalue"]) # Classify regulation status lfc_thresh = 1.0 padj_thresh = 0.05 df_de["sig"] = "NS" df_de.loc[(df_de["log2fc"] > lfc_thresh) & (df_de["pvalue"] < padj_thresh), "sig"] = "Up" df_de.loc[(df_de["log2fc"] < -lfc_thresh) & (df_de["pvalue"] < padj_thresh), "sig"] = "Down" palette = {"NS": "#AAAAAA", "Up": "#D32F2F", "Down": "#1976D2"} sns.set_theme(style="ticks", context="paper", font_scale=1.1) fig, ax = plt.subplots(figsize=(8, 6)) sns.scatterplot(data=df_de, x="log2fc", y="neg_log10_p", hue="sig", palette=palette, alpha=0.6, s=18, linewidth=0, ax=ax) # Threshold lines ax.axhline(-np.log10(padj_thresh), color="black", linestyle="--", linewidth=0.8) ax.axvline( lfc_thresh, color="black", linestyle="--", linewidth=0.8) ax.axvline(-lfc_thresh, color="black", linestyle="--", linewidth=0.8) # Label top 5 most significant genes per direction for direction in ["Up", "Down"]: top = df_de[df_de["sig"] == direction].nlargest(5, "neg_log10_p") for _, row in top.iterrows(): ax.text(row["log2fc"], row["neg_log10_p"] + 0.3, row["gene"], fontsize=6, ha="center", va="bottom", color=palette[direction]) # Annotation counts n_up = (df_de["sig"] == "Up").sum() n_down = (df_de["sig"] == "Down").sum() ax.set_title(f"Volcano Plot | Up: {n_up} Down: {n_down}") ax.set_xlabel("log2 Fold Change") ax.set_ylabel("-log10 p-value") sns.despine(trim=True) plt.tight_layout() plt.savefig("volcano_plot.png", dpi=300, bbox_inches="tight") print(f"Volcano: {n_up} up, {n_down} down — saved volcano_plot.png")
Goal: Compare gene expression (or any continuous measurement) across multiple treatments and time points, showing full distributions plus individual replicates.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(99) genes = ["BRCA1", "TP53", "EGFR"] treats = ["DMSO", "Drug A", "Drug B"] timepoints = ["6h", "24h", "48h"] rows = [] for gene in genes: base_expr = {"BRCA1": 7.5, "TP53": 6.2, "EGFR": 8.1}[gene] for treat in treats: treat_shift = {"DMSO": 0.0, "Drug A": -0.8, "Drug B": 0.6}[treat] for tp in timepoints: tp_shift = {"6h": 0.0, "24h": 0.3, "48h": 0.6}[tp] for _ in range(12): rows.append({ "gene": gene, "treatment": treat, "timepoint": tp, "log2_expr": base_expr + treat_shift + tp_shift + rng.normal(0, 0.5), }) df_mc = pd.DataFrame(rows) sns.set_theme(style="whitegrid", context="paper", font_scale=1.0) g = sns.catplot( data=df_mc, x="timepoint", y="log2_expr", hue="treatment", col="gene", kind="violin", inner="quart", dodge=True, palette="Set2", height=4, aspect=0.9, col_order=genes, order=timepoints, ) # Overlay individual points for ax in g.axes.flat: gene_label = ax.get_title() gene_name = gene_label.split(" = ")[-1] if " = " in gene_label else gene_label subset = df_mc[df_mc["gene"] == gene_name] sns.stripplot( data=subset, x="timepoint", y="log2_expr", hue="treatment", dodge=True, jitter=True, size=2.5, alpha=0.4, palette="dark:black", order=timepoints, legend=False, ax=ax, ) g.set_axis_labels("Timepoint", "log2 Expression") g.set_titles("{col_name}") g.add_legend(title="Treatment") sns.despine(trim=True) g.tight_layout() g.savefig("multigroup_violin.png", dpi=300, bbox_inches="tight") print("Saved multigroup_violin.png")
Goal: Quickly survey pairwise relationships in a multi-feature cell morphology dataset using pairplot, then examine one key pair with a jointplot.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd rng = np.random.default_rng(12) n_per_type = 80 df_morph = pd.DataFrame({ "cell_area_um2": np.concatenate([rng.normal(320, 50, n_per_type), rng.normal(420, 70, n_per_type), rng.normal(280, 40, n_per_type)]), "nucleus_area_um2": np.concatenate([rng.normal(85, 12, n_per_type), rng.normal(110, 18, n_per_type), rng.normal(75, 10, n_per_type)]), "eccentricity": np.abs(np.concatenate([rng.normal(0.6, 0.12, n_per_type), rng.normal(0.8, 0.10, n_per_type), rng.normal(0.5, 0.09, n_per_type)])), "mean_dapi": np.concatenate([rng.exponential(400, n_per_type), rng.exponential(600, n_per_type), rng.exponential(350, n_per_type)]), "cell_line": ["HeLa"] * n_per_type + ["MCF7"] * n_per_type + ["U2OS"] * n_per_type, }) # 1. Pairplot survey g = sns.pairplot(df_morph, hue="cell_line", corner=True, diag_kind="kde", plot_kws={"alpha": 0.5, "s": 15}, palette="Dark2") g.savefig("morphology_pairplot.png", dpi=150) print("Saved morphology_pairplot.png") # 2. Focused jointplot for the most informative pair g2 = sns.jointplot(data=df_morph, x="cell_area_um2", y="nucleus_area_um2", hue="cell_line", kind="scatter", marginal_kws={"fill": True, "alpha": 0.25}, palette="Dark2", alpha=0.6) g2.set_axis_labels("Cell Area (µm²)", "Nucleus Area (µm²)") g2.savefig("morphology_jointplot.png", dpi=150) print("Saved morphology_jointplot.png")
| Parameter | Function(s) | Default | Range / Options | Effect | |-----------|-------------|---------|-----------------|--------| | hue | All plot functions | None | Column name (categorical or continuous) | Color-encodes a variable; triggers automatic legend | | style | scatterplot, lineplot | None | Categorical column name | Encodes variable with marker shape or line dash pattern | | size | scatterplot, lineplot | None | Categorical or continuous column | Encodes variable via point or line size | | col / row | Figure-level only (relplot, displot, catplot, lmplot) | None | Categorical column name | Creates one subplot panel per unique value | | col_wrap | Figure-level only | None | int | Wraps columns onto a new row after N panels | | estimator | barplot, pointplot | "mean" | "mean", "median", any callable | Aggregation function applied within each category | | errorbar | barplot, lineplot, pointplot | ("ci", 95) | "ci", "sd", "se", "pi", None | Error bar type displayed around the estimate | | stat | histplot | "count" | "count", "frequency", "density", "probability" | Normalization applied to histogram bar heights | | bw_adjust | kdeplot, violinplot | 1.0 | 0.1–3.0 | KDE bandwidth multiplier; lower=spikier, higher=smoother | | multiple | histplot, kdeplot | "layer" | "layer", "stack", "dodge", "fill" | How overlapping hue groups are drawn | | inner | violinplot | "box" | "box", "quart", "point", "stick", None | Interior annotation inside the violin body | | standard_scale | clustermap | None | 0 (rows), 1 (columns) | Z-score normalization axis before clustering | | dodge | boxplot, violinplot, stripplot | Varies | True, False | Separate hue-grouped elements along the axis | | context | set_theme() | "notebook" | "paper", "notebook", "talk", "poster" | Scales font and line widths for output medium |
hue, style, size) reads variable names directly from column names. Passing raw arrays loses axis labels and legends. Use pd.melt() to convert wide-form data.set_theme() once at the top of a script: This sets the global style, context, and palette for all subsequent plots, ensuring consistency. Reset to defaults with sns.set_theme().python sns.set_theme(style="ticks", context="paper", font_scale=1.1, rc={"axes.spines.right": False, "axes.spines.top": False})
relplot, catplot) create their own figure and cannot be placed in an existing Axes. When combining different plot types (e.g., scatter + violin + heatmap), allocate a plt.subplots() grid and use axes-level functions with ax=.sns.set_palette("colorblind") or palette="colorblind" produces a palette distinguishable by readers with common color vision deficiencies. For diverging data, use "RdBu_r" or "coolwarm" with center=0.stripplot or swarmplot with alpha=0.4 and small size conveys data density without obscuring the summary statistic.height and aspect, not figsize: Figure-level functions ignore figsize. Use height= (inches per panel) and aspect= (width-to-height ratio per panel). For axes-level, set figsize on the plt.subplots() call.plt.savefig() on a figure-level grid: Figure-level functions return a FacetGrid/JointGrid object. Save it with g.savefig("out.png", dpi=300, bbox_inches="tight"), not plt.savefig(), which may capture a blank figure.When to use: Preparing a multi-panel figure for journal submission or a slide deck.
pythonimport seaborn as sns import matplotlib.pyplot as plt import numpy as np import pandas as pd sns.set_theme(style="ticks", context="paper", font_scale=1.2, rc={"pdf.fonttype": 42, "ps.fonttype": 42}) rng = np.random.default_rng(7) df = pd.DataFrame({ "condition": np.repeat(["Control", "Treated"], 40), "ki67_pct": np.concatenate([rng.normal(18, 4, 40), rng.normal(32, 6, 40)]), "apoptosis": np.concatenate([rng.normal(5, 1.5, 40), rng.normal(12, 2.5, 40)]), }) custom_palette = {"Control": "#4575B4", "Treated": "#D73027"} fig, axes = plt.subplots(1, 2, figsize=(8, 4)) # Panel A sns.boxplot(data=df, x="condition", y="ki67_pct", palette=custom_palette, width=0.45, linewidth=1.2, ax=axes[0]) sns.stripplot(data=df, x="condition", y="ki67_pct", color="black", alpha=0.35, size=3, jitter=True, ax=axes[0]) axes[0].set_ylabel("Ki67 Positive Cells (%)") axes[0].set_xlabel("") axes[0].set_title("A", loc="left", fontweight="bold") # Panel B sns.boxplot(data=df, x="condition", y="apoptosis", palette=custom_palette, width=0.45, linewidth=1.2, ax=axes[1]) sns.stripplot(data=df, x="condition", y="apoptosis", color="black", alpha=0.35, size=3, jitter=True, ax=axes[1]) axes[1].set_ylabel("Apoptotic Cells (%)") axes[1].set_xlabel("") axes[1].set_title("B", loc="left", fontweight="bold") sns.despine(trim=True) plt.tight_layout() plt.savefig("figure1.pdf", dpi=300, bbox_inches="tight") plt.savefig("figure1.png", dpi=300, bbox_inches="tight") print("Saved figure1.pdf and figure1.png at 300 DPI")
When to use: Displaying a gene expression matrix with sample group annotations and hierarchical clustering to reveal co-expression modules.
pythonimport seaborn as sns import matplotlib.pyplot as plt import matplotlib.patches as mpatches import numpy as np import pandas as pd rng = np.random.default_rng(21) n_genes, n_samples = 40, 20 conditions = ["tumor"] * 10 + ["normal"] * 10 # Simulate expression: 3 co-expression modules expr = pd.DataFrame( rng.lognormal(2.5, 0.8, (n_genes, n_samples)), index=[f"GENE{i:03d}" for i in range(n_genes)], columns=[f"{c[0].upper()}{i:02d}" for i, c in enumerate(conditions)], ) # Module 1: genes 0-13 up in tumor expr.iloc[:14, :10] *= 3.0 # Module 2: genes 14-27 down in tumor expr.iloc[14:28, :10] *= 0.3 # Module 3: genes 28-39 unchanged log_expr = np.log2(expr + 1) # Column colors: tumor=red, normal=blue cond_pal = {"tumor": "#C62828", "normal": "#1565C0"} col_colors = [cond_pal[c] for c in conditions] # Row colors: module membership module_pal = {"up": "#EF9A9A", "down": "#90CAF9", "stable": "#C8E6C9"} row_modules = (["up"] * 14) + (["down"] * 14) + (["stable"] * 12) row_colors = [module_pal[m] for m in row_modules] g = sns.clustermap( log_expr, cmap="RdYlBu_r", center=log_expr.values.mean(), standard_scale=0, # z-score per gene (row) method="ward", metric="euclidean", col_colors=col_colors, row_colors=row_colors, figsize=(14, 12), linewidths=0, cbar_pos=(0.02, 0.85, 0.03, 0.12), cbar_kws={"label": "Row z-score"}, dendrogram_ratio=(0.12, 0.08), ) g.ax_heatmap.set_xlabel("Sample", fontsize=10) g.ax_heatmap.set_ylabel("Gene", fontsize=10) g.ax_heatmap.set_title("Gene Expression Clustermap", fontsize=12, pad=80) # Manual legend for column/row annotations legend_handles = [ mpatches.Patch(color="#C62828", label="Tumor"), mpatches.Patch(color="#1565C0", label="Normal"), mpatches.Patch(color="#EF9A9A", label="Up in tumor"), mpatches.Patch(color="#90CAF9", label="Down in tumor"), mpatches.Patch(color="#C8E6C9", label="Stable"), ] g.ax_heatmap.legend(handles=legend_handles, bbox_to_anchor=(1.25, 1.05), loc="upper left", frameon=False, fontsize=9) plt.savefig("clustermap_annotated.png", dpi=300, bbox_inches="tight") print("Saved clustermap_annotated.png")
| Problem | Cause | Solution | |---------|-------|----------| | Legend placed outside plot, clipped in saved file | Figure-level functions place the legend outside by default | Add bbox_inches="tight" to savefig(): g.savefig("out.png", dpi=300, bbox_inches="tight") | | TypeError: FacetGrid.savefig() or blank figure saved | Called plt.savefig() on a figure-level grid that owns its own figure | Use g.savefig(...) instead of plt.savefig(...) | | Overlapping x-axis category labels | Long label strings overlap at default rotation | Add plt.xticks(rotation=45, ha="right") and plt.tight_layout() after the plot call | | ValueError: Could not interpret value ... for parameter 'hue' | Data is in wide-form; hue mapping requires long-form | Convert with df.melt(id_vars=[...], var_name="sample", value_name="expr") | | KDE bandwidth too smooth (loses bimodality) | Default bw_adjust=1.0 over-smooths small datasets | Lower to bw_adjust=0.5; confirm peaks with histplot | | clustermap ignores figsize | Figure-level functions do not accept figsize as a kwarg in older seaborn | Pass figsize as a direct argument: sns.clustermap(..., figsize=(12, 10)) | | Violin plot is a thin line (no shape) | Too few observations for KDE estimation | Switch to kind="box" or kind="strip"; or use cut=0 to restrict KDE to data range | | Colors not distinguishable for many groups | Default palette repeats with >6 categories | Use sns.color_palette("husl", n_colors=N) or "tab20" for up to 20 distinct colors | | Figure-level function ignores ax= parameter | Axes-level distinction: figure-level functions create their own figure | Use the corresponding axes-level function (scatterplot, histplot, etc.) with ax= |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-12 | pass→pass | 8,745 | 20,563 | +135% | 1 | 1 | 0% | 1,661 | 12,758 | +668% | 0 | 0 | — |
case-13 | pass→pass | 7,031 | 10,131 | +44% | 1 | 1 | 0% | 1,450 | 12,940 | +792% | 0 | 0 | — |
case-04 | pass→pass | 17,583 | 22,385 | +27% | 1 | 1 | 0% | 3,278 | 15,550 | +374% | 0 | 0 | — |
case-05 | pass→pass | 10,034 | 13,779 | +37% | 1 | 1 | 0% | 1,913 | 13,829 | +623% | 0 | 0 | — |
case-01 | fail→fail | 16,393 | 31,690 | +93% | 1 | 1 | 0% | 3,120 | 13,639 | +337% | 0 | 0 | — |
case-02 | pass→pass | 17,075 | 12,197 | -29% | 1 | 1 | 0% | 3,296 | 13,431 | +307% | 0 | 0 | — |
case-03 | pass→pass | 24,557 | 11,908 | -52% | 1 | 1 | 0% | 4,367 | 13,254 | +204% | 0 | 0 | — |
case-06 | pass→pass | 11,242 | 7,045 | -37% | 1 | 1 | 0% | 2,373 | 12,502 | +427% | 0 | 0 | — |
case-07 | pass→pass | 14,659 | 7,082 | -52% | 1 | 1 | 0% | 2,759 | 12,450 | +351% | 0 | 0 | — |
case-08 | pass→pass | 5,916 | 5,588 | -6% | 1 | 1 | 0% | 1,006 | 12,155 | +1108% | 0 | 0 | — |
case-09 | pass→pass | 10,663 | 6,817 | -36% | 1 | 1 | 0% | 2,027 | 12,316 | +508% | 0 | 0 | — |
case-10 | pass→pass | 7,810 | 7,244 | -7% | 1 | 1 | 0% | 1,417 | 12,481 | +781% | 0 | 0 | — |
case-11 | fail→pass | 5,211 | 7,286 | +40% | 1 | 1 | 0% | 999 | 12,579 | +1159% | 0 | 0 | — |
case-14 | pass→pass | 15,484 | 29,561 | +91% | 1 | 1 | 0% | 2,666 | 13,591 | +410% | 0 | 0 | — |
case-15 | pass→pass | 11,282 | 8,986 | -20% | 1 | 1 | 0% | 2,285 | 12,877 | +464% | 0 | 0 | — |
case-16 | pass→pass | 6,976 | 7,563 | +8% | 1 | 1 | 0% | 1,379 | 12,631 | +816% | 0 | 0 | — |
case-17 | fail→fail | 4,408 | 5,351 | +21% | 1 | 1 | 0% | 835 | 12,083 | +1347% | 0 | 0 | — |
case-18 | pass→pass | 11,138 | 6,712 | -40% | 1 | 1 | 0% | 2,371 | 12,481 | +426% | 0 | 0 | — |
case-19 | pass→pass | 6,842 | 4,942 | -28% | 1 | 1 | 0% | 1,314 | 12,007 | +814% | 0 | 0 | — |
case-20 | pass→pass | 8,157 | 7,442 | -9% | 1 | 1 | 0% | 1,666 | 12,623 | +658% | 0 | 0 | — |
case-21 | pass→pass | 5,888 | 53,770 | +813% | 1 | 1 | 0% | 1,069 | 12,315 | +1052% | 0 | 0 | — |
case-22 | fail→pass | 10,391 | 10,423 | +0% | 1 | 1 | 0% | 2,216 | 13,166 | +494% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.