Seaborn — NaN in Hue Silently Deletes Rows
3 NaN region values in Seaborn hue deleted all rows for that group, causing a false sales drop.
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- Seaborn is a high-level statistical visualization library built on Matplotlib
- It expects tidy data: one row per observation, one column per variable
- Figure-level functions (relplot, catplot) create FacetGrids and manage subplots automatically
- Axes-level functions (boxplot, scatterplot) give you direct Matplotlib control
- Performance: plotting 10k points takes ~0.5s; for larger datasets, sample or enable rasterization
- Production pitfall: NaN values in hue columns cause silent row drops, distorting group comparisons
- Biggest mistake: calling plt.title() after a figure-level function – the title lands on the wrong panel
Seaborn is a Python data visualization library built on top of Matplotlib that provides a high-level interface for drawing statistical graphics. It exists to solve the friction of creating informative, publication-quality plots from Pandas DataFrames with minimal code — handling aggregation, color mapping, and faceting automatically.
Its core abstraction is the 'tidy data' principle: each row is an observation, each column is a variable. You pass column names to parameters like x, y, hue, col, and row, and Seaborn handles the rest. This makes it ideal for exploratory analysis where you want to quickly see relationships, distributions, and patterns without manually looping or managing subplot axes.
Seaborn operates at two levels: figure-level functions (like relplot, displot, catplot) that create a FacetGrid and manage the entire figure, and axes-level functions (like scatterplot, histplot, boxplot) that plot onto a single Matplotlib Axes. The figure-level functions are more powerful for multi-panel plots but come with a hidden cost: they silently drop rows where any variable used in the plot (including hue, col, row) has a NaN value.
This is not a bug — it's a consequence of Seaborn's internal data reshaping and aggregation pipeline. If you're working with real-world data that has missing values, you need to be aware that your plot may be showing a subset of your data without any warning.
Where Seaborn shines is in rapid iteration: you can go from raw DataFrame to a polished multi-faceted plot in three lines of code. It's the right tool when you want to explore data visually, create statistical summaries (like regression lines, confidence bands, or kernel density estimates), or produce consistent, themed plots for reports.
However, it's not the right choice when you need pixel-level control over every element, when you're building interactive dashboards (use Plotly or Bokeh instead), or when you're working with extremely large datasets (millions of rows) — Seaborn's overhead from data reshaping and Matplotlib rendering becomes a bottleneck. For those cases, consider using datashader for rasterized rendering or dropping down to raw Matplotlib for performance-critical custom plots.
Imagine you have a spreadsheet of 10,000 sales records and your boss asks 'is there a pattern here?' You could stare at the numbers, or you could hand them to an artist who instantly draws a picture that makes the pattern obvious. Seaborn is that artist for Python. It takes raw data — messy, tabular, full of columns — and turns it into publication-quality charts in just a few lines of code. It sits on top of Matplotlib the way a power drill sits on top of a motor: the motor does the hard work, but the drill makes it actually usable.
Every data project hits the same wall: you have the numbers, but you can't see them. A DataFrame full of customer ages, purchase values, and churn flags is just a rectangle of digits until someone visualises it. Seaborn exists precisely for that moment — the moment between 'I have data' and 'I understand data'. It's used daily by data scientists at companies like Spotify and Airbnb to explore datasets before modelling and to communicate findings to non-technical stakeholders.
The real problem Seaborn solves isn't just aesthetics, though its defaults are beautiful. It solves the complexity problem. To draw a grouped box plot with error bars and a sensible colour palette in pure Matplotlib takes 40 lines and a lot of Stack Overflow. In Seaborn it takes three. More importantly, Seaborn understands the concept of 'tidy data' — it knows what a DataFrame is, it reads column names directly, and it maps statistical relationships onto visual properties automatically. That's a fundamentally different abstraction level.
By the end of this article you'll know which Seaborn chart to reach for in six real-world scenarios, why the Figure-level vs Axes-level distinction matters when you're building dashboards, how to customise without fighting the library, and the three mistakes that silently ruin charts for beginners. You'll also be ready to answer the Seaborn questions that come up in data analyst and data science interviews.
How Seaborn's Hue Parameter Silently Drops Data
Seaborn is a Python statistical data visualization library built on Matplotlib that provides a high-level interface for drawing attractive and informative statistical graphics. Its core mechanic is mapping data variables to visual properties like color, size, and style through a declarative API — you specify columns from a DataFrame and Seaborn handles the rest. The hue parameter maps a categorical or numeric column to color, enabling multi-group comparisons in a single plot.
In practice, Seaborn internally calls pandas dropna() on the subset of columns used in the plot call — including the hue column. This means any row with a NaN in the hue column is silently removed before rendering. For a dataset of 100,000 rows with 5% missing hue values, 5,000 rows vanish without warning. The plot looks clean, but the underlying distribution is misrepresented. This behavior is consistent across scatter plots, bar plots, box plots, and relational plots.
Use Seaborn when you need rapid, publication-quality statistical plots with minimal code — especially for exploratory data analysis (EDA) and communicating patterns to non-technical stakeholders. But in production pipelines or any system where data integrity matters, you must explicitly handle missing values before calling Seaborn. Never rely on Seaborn to preserve row counts; always validate your DataFrame's completeness before plotting.
df.isna().sum() before plotting to avoid misleading visualizations.dropna() on the plotting columns — rows with NaN in hue are removed without warning.Seaborn's Mental Model: Tidy Data, Figure-Level vs Axes-Level
Before you write a single line of Seaborn, you need to understand its two core assumptions, because breaking either one causes confusing bugs.
First: Seaborn expects tidy data. That means one observation per row and one variable per column. If your DataFrame has columns called 'Jan_Sales', 'Feb_Sales', 'Mar_Sales', Seaborn will fight you. The correct shape has a 'Month' column and a 'Sales' column — one row per month per product. Pandas' melt() function is your friend here.
Second: Seaborn has two tiers of functions. Axes-level functions like histplot(), scatterplot(), and boxplot() draw onto a single Matplotlib Axes object — they behave like normal Matplotlib and you can combine them freely. Figure-level functions like displot(), relplot(), and catplot() create their own Figure and can produce multi-panel grids via a 'col=' or 'row=' argument. They return a FacetGrid object, not an Axes, which is why calling plt.title() on one produces the wrong result.
Knowing this split stops you spending an hour wondering why your title is in the wrong place or why subplots won't cooperate.
import seaborn as sns import matplotlib.pyplot as plt import pandas as pd # Build a tidy sales DataFrame sales_data = pd.DataFrame({ 'month': ['Jan','Jan','Feb','Feb','Mar','Mar'] * 3, 'region': ['North','South'] * 9, 'revenue': [42000,38000,51000,47000,63000,58000, 39000,41000,49000,52000,61000,66000, 44000,37000,55000,48000,67000,60000] }) # Axes-level example: we control the figure fig, axes = plt.subplots(1, 2, figsize=(12,5)) sns.boxplot(data=sales_data, x='month', y='revenue', hue='region', ax=axes[0]) axes[0].set_title('Revenue by Month and Region (Axes-level)') axes[0].set_ylabel('Revenue (USD)') sns.barplot(data=sales_data, x='month', y='revenue', hue='region', errorbar='sd', ax=axes[1]) axes[1].set_title('Average Revenue with Std Dev (Axes-level)') axes[1].set_ylabel('Mean Revenue (USD)') plt.suptitle('Axes-Level Seaborn: We Own the Figure', fontsize=14, y=1.02) plt.tight_layout() plt.savefig('axes_level_demo.png', dpi=150, bbox_inches='tight') plt.show() print('Axes-level chart saved.') # Figure-level example: catplot manages its own figure grid = sns.catplot(data=sales_data, x='month', y='revenue', col='region', kind='box', height=4, aspect=0.9, palette='muted') grid.set_titles('Region: {col_name}') grid.set_axis_labels('Month', 'Revenue (USD)') grid.figure.suptitle('Figure-Level catplot: Seaborn Owns the Figure', y=1.03) plt.savefig('figure_level_demo.png', dpi=150, bbox_inches='tight') plt.show() print('Figure-level chart saved.')
catplot(), relplot(), or displot(), calling plt.title('My Title') places the title on the last active Axes panel, not the whole figure. Use grid.figure.suptitle('My Title') instead, or grid.set_titles('{col_name}') for per-panel labels.plt.title() on a FacetGrid.Choosing the Right Chart: Six Real-World Scenarios
The most common Seaborn mistake isn't bad syntax — it's reaching for the wrong chart. Here's the decision framework professionals actually use.
Distribution of a single numeric variable? Use histplot() with kde=True to overlay the density curve. It answers 'is this data normally distributed, skewed, or bimodal?' before you choose a statistical test.
Relationship between two numeric variables? scatterplot() with hue= for a third categorical dimension. Add a regression line with lmplot() when you want to communicate correlation to a non-technical audience.
Comparing a numeric variable across categories? boxplot() for showing spread and outliers, violinplot() when sample size is large enough to trust the density estimate (roughly n > 30 per group), and barplot() only when mean + uncertainty is the right summary.
Correlation across many numeric columns? heatmap() on a correlation matrix. This is the chart that identifies multicollinearity before you build a regression model.
Change over time? lineplot() with hue= for multiple groups. Seaborn automatically aggregates and draws confidence intervals when multiple observations exist per x value.
Distribution across two categorical dimensions? heatmap() on a pivot table, or pointplot() with both x= and hue= for overlapping line-point combos.
import seaborn as sns import matplotlib.pyplot as plt import pandas as pd import numpy as np sns.set_theme(style='whitegrid', palette='colorblind', font_scale=1.1) penguins = sns.load_dataset('penguins').dropna() print(f"Dataset shape: {penguins.shape}") print(penguins.head(3)) # Scenario 1: Distribution of flipper length fig, ax = plt.subplots(figsize=(8,4)) sns.histplot(data=penguins, x='flipper_length_mm', hue='species', kde=True, bins=25, alpha=0.5, ax=ax) ax.set_title('Flipper Length Distribution by Species') ax.set_xlabel('Flipper Length (mm)') plt.tight_layout() plt.savefig('scenario1_distribution.png', dpi=150) plt.show() # Scenario 2: Relationship with regression lm_grid = sns.lmplot(data=penguins, x='bill_length_mm', y='bill_depth_mm', hue='species', height=5, aspect=1.3, scatter_kws={'alpha':0.6, 's':40}) lm_grid.set_axis_labels('Bill Length (mm)', 'Bill Depth (mm)') lm_grid.figure.suptitle('Bill Dimensions: Species Show Opposite Trends (Simpson Paradox)', y=1.02) plt.savefig('scenario2_regression.png', dpi=150, bbox_inches='tight') plt.show() # Scenario 3: Numeric across categories fig, axes = plt.subplots(1,2,figsize=(12,5)) sns.violinplot(data=penguins, x='species', y='body_mass_g', hue='sex', split=True, inner='quartile', palette='Set2', ax=axes[0]) axes[0].set_title('Body Mass Distribution (Violin)') sns.boxplot(data=penguins, x='species', y='body_mass_g', hue='sex', palette='Set2', ax=axes[1]) axes[1].set_title('Body Mass Distribution (Box)') plt.suptitle('Same Data, Different Chart — Violin Shows Full Shape', fontsize=13) plt.tight_layout() plt.savefig('scenario3_violin_vs_box.png', dpi=150) plt.show() # Scenario 4: Correlation heatmap numeric_cols = penguins.select_dtypes(include='number') correlation_matrix = numeric_cols.corr() fig, ax = plt.subplots(figsize=(6,5)) sns.heatmap(correlation_matrix, annot=True, fmt='.2f', cmap='coolwarm', vmin=-1, vmax=1, square=True, linewidths=0.5, ax=ax) ax.set_title('Penguin Feature Correlations — Check Before Modelling') plt.tight_layout() plt.savefig('scenario4_heatmap.png', dpi=150) plt.show() print('All charts saved.')
Customising Seaborn Without Fighting It — Themes, Palettes, and Matplotlib Escape Hatches
Seaborn's defaults are intentionally good. The trap beginners fall into is immediately overriding everything and ending up with something worse than the default. The right mental model is: let Seaborn do 80%, then use Matplotlib for the final 20%.
The sns.set_theme() call at the top of your script is the single most powerful line. It sets the background, grid style, font scale, and colour palette for every chart that follows. Choose from five styles: 'darkgrid', 'whitegrid', 'dark', 'white', and 'ticks'. For presentations use 'white'; for exploratory analysis 'whitegrid' helps you read values.
Colour palettes deserve real thought. The 'colorblind' palette is the professional default — it's distinguishable by people with deuteranopia and protanopia (about 8% of men). For sequential data (low to high) use 'Blues' or 'YlOrRd'. For diverging data (negative to positive, like correlations) use 'coolwarm' or 'RdBu_r'. Never use the default rainbow — it implies ordering where none exists.
For anything Seaborn can't do natively, you always have access to the underlying Matplotlib object. Axes-level functions return the Axes; figure-level functions expose their Figure via grid.figure and individual axes via grid.axes_dict.
import seaborn as sns import matplotlib.pyplot as plt import matplotlib.ticker as mticker import pandas as pd import numpy as np # Build a realistic e-commerce monthly metrics DataFrame np.random.seed(42) months = pd.date_range('2023-01', periods=12, freq='MS') channels = ['Organic', 'Paid Search', 'Email', 'Social'] records = [] for channel in channels: base = {'Organic':12000, 'Paid Search':8000, 'Email':5000, 'Social':3000} for i, month in enumerate(months): revenue = base[channel] + np.random.randint(-1500, 3000) + (i * 200) records.append({'month': month, 'channel': channel, 'revenue': revenue}) ecommerce_df = pd.DataFrame(records) # Set a publication-ready theme sns.set_theme(style='white', palette='colorblind', font='DejaVu Sans', font_scale=1.15, rc={'axes.spines.top':False, 'axes.spines.right':False, 'lines.linewidth':2.2}) # Line chart: revenue trend with confidence band fig, ax = plt.subplots(figsize=(11,5)) sns.lineplot(data=ecommerce_df, x='month', y='revenue', hue='channel', markers=True, dashes=False, ax=ax) ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f'${v:,.0f}')) ax.set_title('Monthly Revenue by Channel — 2023', fontsize=15, pad=12) ax.set_xlabel('') ax.set_ylabel('Revenue (USD)') ax.legend(title='Channel', bbox_to_anchor=(1.01,1), loc='upper left') plt.tight_layout() plt.savefig('ecommerce_revenue_trend.png', dpi=150, bbox_inches='tight') plt.show() print('Revenue trend chart saved.') # Palette demo: sequential vs diverging fig, axes = plt.subplots(1,3,figsize=(14,4)) category_totals = ecommerce_df.groupby('channel')['revenue'].sum().reset_index() sns.barplot(data=category_totals, x='channel', y='revenue', palette='colorblind', ax=axes[0]) axes[0].set_title('Colorblind Palette (Categorical Data)') axes[0].yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f'${v/1000:.0f}k')) sns.barplot(data=category_totals, x='channel', y='revenue', palette='Blues_d', ax=axes[1]) axes[1].set_title('Blues_d Palette (Sequential — Implies Rank)') axes[1].yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f'${v/1000:.0f}k')) brand_palette = ['#0057FF', '#FF6B35', '#2EC4B6', '#FFBF00'] sns.barplot(data=category_totals, x='channel', y='revenue', palette=brand_palette, ax=axes[2]) axes[2].set_title('Custom Brand Palette (Hex Codes)') axes[2].yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f'${v/1000:.0f}k')) for ax in axes: ax.set_xlabel('') ax.set_ylabel('Total Revenue') sns.despine(ax=ax) plt.suptitle('Palette Choice Changes the Story', fontsize=13, y=1.02) plt.tight_layout() plt.savefig('palette_comparison.png', dpi=150, bbox_inches='tight') plt.show() print('Palette comparison saved.')
Pairplots and FacetGrids — Exploring Entire Datasets in One Call
Once you've got individual charts under control, Seaborn's real superpower for exploratory data analysis is the multi-chart grid. Two functions deliver this: pairplot() and FacetGrid.
pairplot() is the tool you run on a new dataset before you do anything else. It draws every numeric column against every other numeric column — scatter plots off-diagonal, distributions on-diagonal — and colour-codes by a categorical variable. In five seconds you can see which pairs of features are linearly related, which ones cluster by class, and which ones are skewed. It's the fastest possible dataset overview.
FacetGrid is the manual version. You control exactly which variable goes on rows, which goes on columns, and then you map any Axes-level Seaborn or Matplotlib function onto every panel. This is how you build dashboards programmatically — one loop builds 12 charts, perfectly aligned, with shared axes.
Both are figure-level, so the plt.title() caveat from Section 1 applies. The payoff is that the layout, spacing, and legend are all handled for you.
import seaborn as sns import matplotlib.pyplot as plt import matplotlib.ticker as mticker import pandas as pd sns.set_theme(style='ticks', palette='colorblind', font_scale=1.0) diamonds = sns.load_dataset('diamonds') diamond_sample = diamonds.sample(n=1500, random_state=99).reset_index(drop=True) print(f"Diamonds sample: {diamond_sample.shape}") print(diamond_sample[['carat','price','depth','cut']].head(3)) # Pairplot: dataset overview in one call pair_grid = sns.pairplot(diamond_sample[['carat','price','depth','table','cut']], hue='cut', diag_kind='kde', plot_kws={'alpha':0.4, 's':15}, height=2.2) pair_grid.figure.suptitle('Diamond Features Pairplot — Cut Quality Colour-Coded', y=1.01, fontsize=13) plt.savefig('diamond_pairplot.png', dpi=130, bbox_inches='tight') plt.show() print('Pairplot saved.') # FacetGrid: custom multi-panel chart cut_grid = sns.FacetGrid(data=diamond_sample, col='cut', col_order=['Fair','Good','Very Good','Premium','Ideal'], height=3.5, aspect=0.75, sharey=True) cut_grid.map_dataframe(sns.scatterplot, x='carat', y='price', alpha=0.3, s=12, color='steelblue') cut_grid.map_dataframe(sns.regplot, x='carat', y='price', scatter=False, line_kws={'color':'crimson', 'linewidth':1.8}) cut_grid.set_axis_labels('Carat', 'Price (USD)') cut_grid.figure.suptitle('Carat vs Price by Cut Quality — Steeper Slopes = Better Value Per Carat', y=1.02, fontsize=12) plt.savefig('diamond_facetgrid.png', dpi=130, bbox_inches='tight') plt.show() print('FacetGrid saved.')
Handling Large Datasets and Plot Performance
Seaborn's default settings work well for datasets up to tens of thousands of points. Beyond that, performance degrades and patterns become invisible due to overplotting. Here are the strategies used in production.
First, always sample when exploring. df.sample(n=3000) preserves the shape of the data and reduces rendering time from minutes to seconds. Use the random_state parameter for reproducibility.
Second, use transparency and small markers. scatterplot with alpha=0.1, s=5 can show density without a solid blob. For even larger data, switch to a hexbin histogram: sns.histplot(x='col1', y='col2', kind='hex') bins the points into hexagons and colours by count.
Third, enable rasterization for vector graphics. When saving to PDF or SVG, vectorised scatter plots with 100k points can produce files that crash viewers. Set scatter_kws={'rasterized': True} to rasterize only the scatter layer while keeping axes and labels as vectors.
Fourth, downsample along time series. For longitudinal data, resample to a lower frequency (e.g., hourly to daily averages) before plotting. Use pandas resample() and aggregate by mean.
Finally, use catplot with kind='box' and multiple plots via col= to split data into manageable panels instead of one overwhelming chart.
import seaborn as sns import matplotlib.pyplot as plt import pandas as pd import numpy as np # Simulate a large dataset (100k rows) np.random.seed(42) n = 100_000 large_df = pd.DataFrame({ 'x': np.random.normal(50, 15, n), 'y': np.random.normal(200, 30, n), 'category': np.random.choice(['A','B','C'], n), 'region': np.random.choice(['North','South','East','West'], n) }) # Strategy 1: Sample before plotting sample = large_df.sample(n=3000, random_state=42) fig, ax = plt.subplots(figsize=(8,5)) sns.scatterplot(data=sample, x='x', y='y', hue='category', alpha=0.5, s=10, ax=ax) ax.set_title('Sampled to 3000 points – patterns clear') plt.tight_layout() plt.savefig('sampled_scatter.png', dpi=150, bbox_inches='tight') plt.show() # Strategy 2: Hexbin for full dataset fig, ax = plt.subplots(figsize=(8,5)) sns.histplot(data=large_df, x='x', y='y', kind='hex', bins=40, ax=ax, cmap='Blues') ax.set_title('Hexbin with all 100k points – density visible') plt.tight_layout() plt.savefig('hexbin.png', dpi=150, bbox_inches='tight') plt.show() # Strategy 3: Rasterized scatter for vector output fig, ax = plt.subplots(figsize=(8,5)) sns.scatterplot(data=large_df.sample(10000), x='x', y='y', alpha=0.3, s=5, rasterized=True, ax=ax) ax.set_title('Rasterized scatter – small PDF size') plt.tight_layout() plt.savefig('rasterized_scatter.pdf', bbox_inches='tight') plt.show() print('Large data strategies demonstrated.')
The Hidden Cost of Defaults: Seaborn's Statistical Aggregations
Seaborn silently aggregates your data. When you call barplot or pointplot with multiple observations per category, Seaborn defaults to estimator=mean and ci=95. That confidence interval is a bootstrap (n_boot=10000). On a 100k-row dataset, that's 10 million resamples. Your "simple bar chart" just burned 12GB of RAM. I learned this when a cron job crashed at 3AM — the bar plot was aggregating by default while we thought it was plotting raw values. The fix: pass estimator=None to disable aggregation, or use sns.barplot(..., ci=None) to skip confidence intervals. For categorical plots where you control the aggregation upstream, always disable Seaborn's. You control when stats happen, not the other way around.
// io.thecodeforge import seaborn as sns, pandas as pd, numpy as np df = pd.DataFrame({'group': np.repeat(['A','B'], 50000), 'value': np.random.normal(0, 1, 100000)}) # Bad: seaborn bootstraps 10k times on 100k rows sns.barplot(data=df, x='group', y='value', ci=95) # Good: disable aggregation entirely sns.barplot(data=df, x='group', y='value', estimator=None) # Better: pre-aggregate yourself agg_df = df.groupby('group')['value'].agg(['mean', 'std']).reset_index() sns.barplot(data=agg_df, x='group', y='mean', yerr=agg_df['std'])
ci=None in production plotting pipelines unless you explicitly need inference.Why Your Heatmap Eats Memory: The Tripwire of Wide-Form Data
Heatmaps are the swiss army knife of exploratory analysis, but they're also a memory trap. Seaborn's heatmap converts your wide-form DataFrame into a 2D array internally, then calls imshow(). If your DataFrame has 1000 columns × 1000 rows, that's 1 million cells. At 64 bits per cell, that's 8MB for the data. But imshow rasterizes it — now it's 4 bytes per pixel × display resolution, plus interpolation buffers. A 4K screen: 3840×2160 = 8.3M pixels, each with RGBA = 33MB. Add Python overhead and you're at 120MB for a single heatmap. The fix: downsample before you plot. Don't show 1000 categories on an axis that can only display 20 labels. Use df.sample(1000, axis=0) or cluster then aggregate. Better yet: pass rasterized=True to heatmap to force vector-to-raster conversion, saving both memory and file size on exports.
// io.thecodeforge import seaborn as sns, pandas as pd, numpy as np # Simulating 1000x1000 wide-form data wide_df = pd.DataFrame(np.random.rand(1000, 1000)) # Bad: full resolution, memory spikes ~120MB # sns.heatmap(wide_df) # Good: downsample aggressively sampled = wide_df.iloc[:100, :100] # 10k cells sns.heatmap(sampled, rasterized=True, cbar=False) # Best: cluster and aggregate before plotting from scipy.cluster.hierarchy import linkage clustered = wide_df.iloc[linkage(wide_df).flatten()[:50], :50] sns.clustermap(clustered, rasterized=True, figsize=(8,8))
The One Plot Order That Silently Corrupts Your Story: How Seaborn Orders Categories
Seaborn orders categorical axes by default — but not how you expect. For string columns, it orders chronologically by appearance in the DataFrame. For numbers stored as strings, it orders lexicographically (1, 10, 100, 2, 20...). I watched a data scientist present "sales by quarter" that showed Q10 before Q2. The culprit: quarter was a string column, and 'Q10' < 'Q2' alphabetically. The fix is trivial once you know: convert to a categorical with explicit ordering using pd.Categorical, or pass the order parameter to every categorical plot. For temporal data, convert to datetime and use sort_values() before plotting. Never assume Seaborn reads your mind — it reads your dtypes. When building dashboards that auto-generate plots, always wrap your category column in pd.Categorical(df['col'], ordered=True) with a preset category list.
// io.thecodeforge import seaborn as sns, pandas as pd df = pd.DataFrame({ 'quarter': ['Q1', 'Q10', 'Q2', 'Q11'], 'sales': [100, 200, 150, 250] }) # Bad: lexicographic order (Q1, Q10, Q11, Q2) sns.boxplot(data=df, x='quarter', y='sales') # Good: force correct ordering correct_order = ['Q1', 'Q2', 'Q10', 'Q11'] sns.boxplot(data=df, x='quarter', y='sales', order=correct_order) # Better: make it permanent from pandas.api.types import CategoricalDtype quarter_cat = CategoricalDtype(categories=correct_order, ordered=True) df['quarter'] = df['quarter'].astype(quarter_cat) sns.boxplot(data=df, x='quarter', y='sales')
order parameter or by casting to pd.Categorical with ordered=True. Never trust default string ordering.The Missing Sales Data That Killed a Quarterly Report
- Seaborn silently drops rows with NaN in any column used for plotting — hue, x, y, size, style.
- Always inspect missing values with
df.isna().sum() before visualization. - Document your data-cleaning decisions: drop, fill, or flag? Each changes the story the chart tells.
plt.title() after a figure-level function like catplot or relplot. Instead, use grid.figure.suptitle('Your Title') or grid.set_titles('{col_name}').fillna() or dropna() before plotting.ax.get_xticks()[::2]).sns.set_palette() but then used an Axes-level function without passing hue=, Seaborn uses the default Matplotlib colour cycle. Pass palette='colorblind' directly to the plotting function, or set hue to a categorical column.grid.figure.suptitle('My Title', y=1.02, fontsize=14)df['hue_col'].isna().sum()df['hue_col'] = df['hue_col'].fillna('Unknown')df_sample = df.sample(n=3000, random_state=42)sns.scatterplot(data=df_sample, ..., rasterized=True)plt.xticks(rotation=45, ha='right')| Feature / Aspect | Axes-Level Functions (e.g. boxplot) | Figure-Level Functions (e.g. catplot) |
|---|---|---|
| Returns | Matplotlib Axes object | FacetGrid object |
Use plt.title()? | Yes — works as expected | No — use grid.figure.suptitle() |
| Multi-panel grids | Manual (plt.subplots) | Built-in via col=, row= params |
| Combine with other charts | Easy — pass ax= param | Harder — use .map_dataframe() |
| Best for | Dashboard panels, custom layouts | Exploratory faceting, quick multi-group views |
| Legend control | Full Matplotlib control | Via grid.add_legend() method |
| Figure size control | figsize on plt.subplots() | height= and aspect= params |
| File | Command / Code | Purpose |
|---|---|---|
| seaborn_mental_model.py | sales_data = pd.DataFrame({ | Seaborn's Mental Model |
| seaborn_chart_selection.py | sns.set_theme(style='whitegrid', palette='colorblind', font_scale=1.1) | Choosing the Right Chart |
| seaborn_customisation.py | np.random.seed(42) | Customising Seaborn Without Fighting It |
| seaborn_pairplot_facetgrid.py | sns.set_theme(style='ticks', palette='colorblind', font_scale=1.0) | Pairplots and FacetGrids |
| seaborn_large_data.py | np.random.seed(42) | Handling Large Datasets and Plot Performance |
| aggregation_trap.py | df = pd.DataFrame({'group': np.repeat(['A','B'], 50000), | The Hidden Cost of Defaults |
| heatmap_memory.py | wide_df = pd.DataFrame(np.random.rand(1000, 1000)) | Why Your Heatmap Eats Memory |
| category_order.py | df = pd.DataFrame({ | The One Plot Order That Silently Corrupts Your Story |
Key takeaways
Common mistakes to avoid
5 patternsCalling plt.title() after a figure-level function
plt.title() works fine.Not handling NaN in hue column
Plotting too many points without sampling
Using rainbow palette for categorical data
Not melting wide-format data before plotting
pd.melt() to transform wide data into long format with a 'month' column and a 'sales' column.Interview Questions on This Topic
What is the difference between figure-level and axes-level functions in Seaborn? Give examples.
grid.figure.suptitle() to set the title, not plt.title().How does Seaborn handle missing data (NaN) in the hue column? What should you do to avoid problems?
df.isna().sum() before plotting. Either fill missing values (df['col'].fillna('Unknown')) or explicitly drop them (df.dropna(subset=['col'])). Document your decision because fill vs drop changes the story.Explain the concept of tidy data and why Seaborn requires it. How do you reshape non-tidy data?
pd.melt() to unpivot wide tables into long format. For example, if you have columns Jan_Sales, Feb_Sales, melt them into 'month' and 'sales' columns. Tidy data is not just for Seaborn — it's a principle for all statistical analysis.What performance optimisation strategies would you recommend for Seaborn plots on large datasets?
resample(). 5) Use FacetGrid to split data into manageable panels. For >1M points, switch to datashader or plotly. Always test rendering time — if it exceeds 5 seconds, reduce data.How do you create a multi-panel chart with Seaborn where each panel shows a different category? Give code example using FacetGrid.
grid.figure.suptitle() and grid.set_titles(). This pattern is ideal for report generation where every panel needs the same analysis.Frequently Asked Questions
You called plt.title() after a figure-level function (catplot, relplot, etc.). Figure-level functions return a FacetGrid object, not a matplotlib Axes. Use grid.figure.suptitle('Your Title') instead.
Check for NaN in the hue column using df['hue_col'].isna().sum(). Seaborn silently drops rows with NaN. Fill missing values with df['hue_col'].fillna('Unknown') or use before plotting.dropna()
Use palette='colorblind' in Seaborn. It uses the Wong (2011) colour set that is distinguishable by people with the most common forms of colour vision deficiency (deuteranopia, protanopia). It's the professional default.
For axes-level functions, you get back an Axes object – use . For figure-level, access individual axes via ax.annotate()grid.axes_dict[category] or grid.axes.flat[index]. You have full matplotlib access after the Seaborn call.
is a figure-level function that creates a FacetGrid and adds a regression line per hue group – it's great for multi-panel communication. lmplot() is an axes-level function that draws a regression line on a single axes, giving you more control over the underlying matplotlib figure.regplot()
20+ years shipping production Python across data and backend systems. Notes here come from systems that actually shipped.
That's Python Libraries. Mark it forged?
6 min read · try the examples if you haven't