Saturday, September 26, 2026
HomeData ScienceMatplotlib and Seaborn Fundamentals – Charts, Statistical Plots and EDA Guide

Matplotlib and Seaborn Fundamentals – Charts, Statistical Plots and EDA Guide

Table of Content

Matplotlib and Seaborn are the two core Python visualisation libraries that every data scientist uses daily. Matplotlib is the foundation layer — it gives you complete control over every element of a figure, making it the right choice for publication-quality plots, custom layouts, and figures that need precise formatting. Seaborn is built on Matplotlib and provides a high-level interface for statistical graphics — with sensible defaults, built-in colour palettes, and functions that handle grouping, aggregation, and confidence intervals automatically. Knowing both deeply, and knowing when to use each, is a daily productivity multiplier for data science work.

This guide focuses on the fundamentals and most commonly used patterns. For interactive charts (Plotly), dashboard design principles, and data storytelling techniques, see our Complete Data Visualization Guide. For deploying interactive dashboards built with these libraries, our Streamlit Deployment guide covers embedding matplotlib/seaborn figures in web apps. Time series visualisation patterns are covered in our Time Series Analysis guide. Visualisation for EDA connects directly to our Feature Engineering guide.

Matplotlib Object Model — Figure, Axes, Artists

Matplotlib has two APIs: the state-machine pyplot API (plt.plot(), plt.title()) and the object-oriented API (fig, ax = plt.subplots(); ax.plot(); ax.set_title()). Always use the object-oriented API for anything beyond a quick exploratory chart — it is explicit, debuggable, and the only practical option for multi-panel figures, custom layouts, and embedded charts. The object hierarchy: Figure is the entire canvas; Axes is one plot area within the figure (a figure can have many axes); Artists are everything drawn on the canvas (lines, text, patches, collections).

import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
import pandas as pd

# ── FIGURE LAYOUT PATTERNS ────────────────────────────────────────────

# Pattern 1: simple single axes
fig, ax = plt.subplots(figsize=(8, 5))

# Pattern 2: grid of axes
fig, axes = plt.subplots(2, 3, figsize=(15, 8), sharex=False, sharey=False)
# axes is a 2D numpy array — access with axes[row, col]

# Pattern 3: complex layout with gridspec
fig = plt.figure(figsize=(14, 8))
gs  = fig.add_gridspec(2, 3, hspace=0.4, wspace=0.3)
ax_main  = fig.add_subplot(gs[0, :])      # top row: full width
ax_bot1  = fig.add_subplot(gs[1, 0])      # bottom left
ax_bot2  = fig.add_subplot(gs[1, 1:])     # bottom right: spans 2 cols

# ── COMMON CHART TYPES ─────────────────────────────────────────────────

# Line chart with multiple series, custom styling
dates   = pd.date_range('2026-01-01', periods=52, freq='W')
sales_a = np.cumsum(np.random.randn(52)) + 100
sales_b = np.cumsum(np.random.randn(52)) + 90

fig, ax = plt.subplots(figsize=(11, 5))
ax.plot(dates, sales_a, color='#2563EB', linewidth=2, label='Product A', zorder=3)
ax.plot(dates, sales_b, color='#DC2626', linewidth=2, label='Product B',
        linestyle='--', zorder=3)
ax.fill_between(dates, sales_a, sales_b, alpha=0.08, color='#2563EB')

# Annotations
peak_idx = np.argmax(sales_a)
ax.annotate('Peak', xy=(dates[peak_idx], sales_a[peak_idx]),
            xytext=(dates[peak_idx+4], sales_a[peak_idx]+5),
            arrowprops=dict(arrowstyle='->', color='gray'),
            fontsize=9, color='gray')

# Styling
ax.spines[['top', 'right']].set_visible(False)
ax.set_title('Weekly Sales Performance 2026', fontsize=13, fontweight='bold', pad=12)
ax.set_xlabel('Week'); ax.set_ylabel('Sales (INR Lakhs)')
ax.legend(frameon=False)
ax.grid(axis='y', alpha=0.3, linestyle='--')
ax.xaxis.set_major_formatter(plt.matplotlib.dates.DateFormatter('%b'))
plt.tight_layout(); plt.savefig('sales_trend.png', dpi=150, bbox_inches='tight')
plt.show()

# ── HORIZONTAL BAR CHART (best for ranked categories) ─────────────────
categories = ['Electronics', 'Clothing', 'Home & Garden', 'Books', 'Sports', 'Toys']
values     = [485, 320, 275, 198, 155, 112]
sorted_idx = np.argsort(values)
cats_sorted = [categories[i] for i in sorted_idx]
vals_sorted = [values[i] for i in sorted_idx]
colors = ['#2563EB' if v == max(values) else '#93C5FD' for v in vals_sorted]

fig, ax = plt.subplots(figsize=(9, 5))
bars = ax.barh(cats_sorted, vals_sorted, color=colors, edgecolor='white', height=0.6)

# Data labels
for bar, val in zip(bars, vals_sorted):
    ax.text(val + 6, bar.get_y() + bar.get_height()/2,
            str(val) + 'L', va='center', fontsize=9, color='#374151')

ax.spines[['top', 'right', 'left']].set_visible(False)
ax.set_xlabel('Revenue (INR Lakhs)')
ax.set_title('Revenue by Category — Q3 2026', fontweight='bold')
ax.tick_params(axis='y', length=0)
plt.tight_layout(); plt.show()

Seaborn — Statistical Charts and EDA

import seaborn as sns

# Set a consistent theme for the notebook
sns.set_theme(style='whitegrid', palette='husl', font_scale=1.05)

# ── DISTRIBUTION PLOTS ────────────────────────────────────────────────

fig, axes = plt.subplots(1, 3, figsize=(16, 5))

# Histogram with KDE
sns.histplot(df['revenue'], kde=True, bins=35, ax=axes[0],
             color='steelblue', edgecolor='white')
axes[0].set_title('Revenue Distribution')

# Box plot with individual points (strip)
sns.boxplot(data=df, x='region', y='revenue', ax=axes[1],
            palette='Blues', flierprops=dict(marker='x', color='gray', alpha=0.5))
axes[1].set_title('Revenue by Region')
axes[1].tick_params(axis='x', rotation=30)

# Violin plot — shows full distribution shape
sns.violinplot(data=df, x='tier', y='revenue', ax=axes[2],
               palette='Pastel1', inner='quartile', cut=0)
axes[2].set_title('Revenue by Customer Tier')

plt.suptitle('Revenue Distribution Analysis', fontsize=13, fontweight='bold', y=1.02)
plt.tight_layout(); plt.show()

# ── RELATIONSHIP PLOTS ────────────────────────────────────────────────

# Scatter with regression line and confidence band
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

sns.regplot(data=df, x='marketing_spend', y='revenue',
            ax=axes[0], scatter_kws={'alpha': 0.4, 's': 20},
            line_kws={'color': '#DC2626', 'linewidth': 2})
axes[0].set_title('Revenue vs Marketing Spend')

# Scatter coloured by category with marginal distributions
sns.scatterplot(data=df, x='tenure_months', y='clv', hue='tier',
                palette='tab10', ax=axes[1], alpha=0.6, s=30)
axes[1].set_title('CLV vs Tenure by Tier')
plt.tight_layout(); plt.show()

# ── HEATMAP (correlation matrix) ─────────────────────────────────────

fig, ax = plt.subplots(figsize=(10, 8))
corr = df.select_dtypes('number').corr()
mask = np.triu(np.ones_like(corr, dtype=bool))   # hide upper triangle
sns.heatmap(corr, mask=mask, annot=True, fmt='.2f',
            cmap='RdYlBu_r', center=0, vmin=-1, vmax=1,
            linewidths=0.5, ax=ax, cbar_kws={'shrink': 0.8})
ax.set_title('Feature Correlation Matrix', fontweight='bold')
plt.tight_layout(); plt.show()

# ── FACET GRID — same plot across subgroups ───────────────────────────
g = sns.FacetGrid(df, col='region', col_wrap=3, height=3.5, aspect=1.3,
                  sharey=False)
g.map_dataframe(sns.lineplot, x='month', y='revenue',
                hue='year', palette='Blues_d', linewidth=1.5)
g.add_legend()
g.set_titles(col_template='{col_name}')
g.set_axis_labels('Month', 'Revenue (INR L)')
g.figure.suptitle('Monthly Revenue by Region', y=1.02, fontweight='bold')
plt.tight_layout(); plt.show()

# ── PAIR PLOT — all pairwise relationships for EDA ────────────────────
pair_grid = sns.pairplot(
    df[['revenue', 'cac', 'clv', 'tenure_months', 'tier']],
    hue='tier', diag_kind='kde',
    plot_kws={'alpha': 0.4, 's': 15},
    palette='tab10'
)
pair_grid.figure.suptitle('Pairwise Feature Relationships', y=1.02)
plt.show()

Saving, Styling, and Common Pitfalls

TaskCodeNotes
Save figureplt.savefig(‘out.png’, dpi=150, bbox_inches=’tight’)bbox_inches=’tight’ prevents clipped labels
Set global styleplt.style.use(‘seaborn-v0_8-whitegrid’)Apply before creating figures
Custom colour palettepalette = [‘#2563EB’, ‘#DC2626’, ‘#16A34A’]Use hex colours for brand consistency
Remove spinesax.spines[[‘top’,’right’]].set_visible(False)Reduces chart junk
Rotate x-tick labelsax.tick_params(axis=’x’, rotation=45)For long category labels
Format y-axis as currencyax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x,p: f’INR {x:,.0f}’))Use mticker.FuncFormatter
Two y-axesax2 = ax.twinx()Useful for bar+line combo charts
Figure title positionfig.suptitle(‘Title’, y=1.02, fontsize=14)y > 1 moves above subplots

Most common Matplotlib mistakes: Using plt.plot() (pyplot state machine) when you have multiple subplots — switch to the OO API. Forgetting plt.tight_layout() — causes overlapping labels. Not setting figsize — the default 6.4×4.8 inches is rarely right. Using a line chart for unordered categories (use bar chart). Choosing a rainbow colormap (jet) — use viridis, Blues, or RdYlBu for accessibility. For deeper visualisation design principles (data-ink ratio, pre-attentive attributes, choosing the right chart type) and interactive Plotly charts, our Complete Data Visualization Guide covers the full picture. For building Streamlit dashboards that embed these charts interactively, see our Streamlit Deployment guide.

Leave feedback about this

  • Rating

Durgesh Kekare
Durgesh Kekarehttps://www.dataexpertise.in
Durgesh Kekare is a data science educator and founder of DataExpertise.in. With expertise in Python, machine learning, and analytics, he helps 10,000+ learners break into data careers.

Latest Posts

List of Categories