| name | seaborn |
| description | [Applies to: **/*.py] This guide defines best practices for using Seaborn in Python for data visualization, emphasizing modern, reproducible, and performant approaches in AI/ML pipelines. |
| source | cursor_mdc |
seaborn Best Practices
Seaborn is the definitive library for statistical data visualization in Python. This guide outlines our team's mandatory best practices to ensure consistent, reproducible, and high-quality plots in all AI/ML projects.
1. Code Organization & Project-Wide Styling
Always configure Seaborn's global style settings once per project. Centralize this in a dedicated styles.py module and import it. This guarantees a uniform visual language across all outputs.
❌ BAD: Ad-hoc styling in every script/notebook
import seaborn as sns
import matplotlib.pyplot as plt
sns.set_theme(style="darkgrid", palette="viridis")
✅ GOOD: Centralized and imported styling
import seaborn as sns
import matplotlib.pyplot as plt
def apply_seaborn_defaults():
"""Applies project-wide Seaborn and Matplotlib styling."""
sns.set_theme(
context="talk",
style="whitegrid",
palette="deep"
)
plt.rcParams["figure.figsize"] = (8, 5)
plt.rcParams["figure.dpi"] = 150
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
from .styles import apply_seaborn_defaults
apply_seaborn_defaults()
2. Prefer Figure-Level Functions
For complex layouts, especially with faceting, always use Seaborn's figure-level functions (relplot, catplot, pairplot, lmplot). They handle figure creation, axis management, and legends automatically, ensuring consistency.
❌ BAD: Manual subplots with axes-level functions for faceting
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
sns.scatterplot(data=df[df['category'] == 'A'], x='x', y='y', ax=axes[0])
sns.scatterplot(data=df[df['category'] == 'B'], x='x', y='y', ax=axes[1])
plt.tight_layout()
plt.show()
✅ GOOD: relplot for automatic faceting and consistent sizing
import pandas as pd
import numpy as np
data = {
'x': np.random.rand(100),
'y': np.random.rand(100) + np.random.rand(100),
'category': np.random.choice(['A', 'B'], 100),
'group': np.random.choice(['X', 'Y'], 100)
}
df = pd.DataFrame(data)
g = sns.relplot(
data=df, x='x', y='y',
col='category',
hue='group',
kind='scatter',
height=4, aspect=1.2
)
g.set_axis_labels("Feature X", "Feature Y")
g.set_titles("Category: {col_name}")
plt.tight_layout()
plt.show()
3. Data Format: Long-Form is King
Seaborn thrives on "tidy" or "long-form" data. Ensure your Pandas DataFrames are structured with one observation per row and variables as columns. This allows Seaborn to infer groupings automatically, reducing boilerplate.
❌ BAD: Wide-form data requiring melt or manual iteration
wide_df = pd.DataFrame({
'id': [1, 2], 'value_A': [10, 20], 'value_B': [15, 25]
})
melted_df = wide_df.melt(id_vars=['id'], var_name='metric', value_name='value')
sns.barplot(data=melted_df, x='metric', y='value')
plt.show()
✅ GOOD: Data already in long-form
long_df = pd.DataFrame({
'id': [1, 2, 1, 2],
'metric': ['value_A', 'value_A', 'value_B', 'value_B'],
'value': [10, 20, 15, 25]
})
sns.barplot(data=long_df, x='metric', y='value')
plt.show()
4. Performance for Large Datasets
For datasets with millions of rows, plotting every point can be slow and create over-plotting. Optimize for clarity and speed.
- Down-sample: Plot a representative subset for initial exploration.
- Binning/Aggregation: Summarize data before plotting (e.g.,
histplot with bins, kdeplot, lineplot on aggregated data).
- Limit
hue: Excessive hue categories lead to cluttered legends and slow rendering.
- Efficient Data Loading: Use
polars or duckdb for faster initial data processing if pandas is a bottleneck.
sampled_df = large_df.sample(n=100_000, random_state=42)
sns.scatterplot(data=sampled_df, x='feature1', y='feature2', alpha=0.5)
plt.show()
daily_avg = large_df.groupby('date')['value'].mean().reset_index()
sns.lineplot(data=daily_avg, x='date', y='value')
plt.show()
5. Type Hints for Clarity
Always use explicit type hints for functions that generate plots. This improves code readability, maintainability, and enables static analysis.
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from typing import Optional, Union
def plot_feature_distribution(
df: pd.DataFrame,
feature_col: str,
hue_col: Optional[str] = None,
title: str = "Feature Distribution"
) -> plt.Axes:
"""
Generates a histogram and KDE plot for a given feature.
Args:
df: Input DataFrame.
feature_col: Name of the column to plot.
hue_col: Optional column for grouping.
title: Plot title.
Returns:
The Matplotlib Axes object.
"""
fig, ax = plt.subplots(figsize=(10, 6))
sns.histplot(data=df, x=feature_col, hue=hue_col, kde=True, ax=ax)
ax.set_title(title)
plt.tight_layout()
return ax
tips = sns.load_dataset("tips")
plot_feature_distribution(tips, "total_bill", hue_col="sex", title="Total Bill Distribution by Sex")
plt.show()
6. Reproducibility: Virtual Environments & Packaging
Ensure full reproducibility of your plotting environment.
- Virtual Environments: Always use
venv or conda environments.
requirements.txt: Pin all dependencies (e.g., seaborn, matplotlib, pandas, numpy) to exact versions using pip freeze > requirements.txt.
python -m venv .venv
source .venv/bin/activate
pip install seaborn==0.13.2 matplotlib==3.8.2 pandas==2.1.4 numpy==1.26.2
pip freeze > requirements.txt
7. Testing: Visual Regression
For critical visualizations in production pipelines, implement visual regression tests. Compare hashes of generated PNGs or use a dedicated library (e.g., pytest-mpl) to detect unintended aesthetic changes caused by library updates or code modifications.
import hashlib
import os
import seaborn as sns
import matplotlib.pyplot as plt
import pandas as pd
from your_project.styles import apply_seaborn_defaults
def generate_plot_image(filename: str):
apply_seaborn_defaults()
df = sns.load_dataset("iris")
sns.scatterplot(data=df, x="sepal_length", y="sepal_width", hue="species")
plt.savefig(filename, bbox_inches="tight")
plt.close()
def test_iris_scatter_plot_regression():
expected_image_path = "tests/baseline_iris_scatter.png"
generated_image_path = "tests/temp_iris_scatter.png"
generate_plot_image(generated_image_path)
with open(expected_image_path, "rb") as f_expected:
expected_hash = hashlib.md5(f_expected.read()).hexdigest()
with open(generated_image_path, "rb") as f_generated:
generated_hash = hashlib.md5(f_generated.read()).hexdigest()
assert expected_hash == generated_hash, "Iris scatter plot has changed visually!"
os.remove(generated_image_path)
8. Documentation: Clear Docstrings
Document every plotting function with a clear docstring. Describe the data source, plot type, purpose, statistical assumptions, parameters, return values, and any side effects (e.g., plt.show()). This makes code audit-friendly and maintainable.