Pandas Plotting: Built-in Visualization Guide
Create effective charts directly from Pandas DataFrames with plot(), including line, bar, histogram, scatter, box plots, and customization tips.
What you'll learn
- ✓How to create common chart types directly from DataFrames
- ✓When to use line, bar, scatter, histogram, and box plots
- ✓How to customize axes, labels, colors, and layout
- ✓Tips for making publication-ready charts with minimal code
Prerequisites
- •Basic Pandas knowledge (DataFrames, Series)
- •Pandas and matplotlib installed (pip install pandas matplotlib)
Pandas has a built-in plotting interface that wraps matplotlib. It is not as flexible as Seaborn or Plotly, but it is the fastest way to visualize your data during analysis. One line of code gets you a chart. A few more lines make it presentable.
Setup
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Sample data: monthly sales
sales = pd.DataFrame({
"month": pd.date_range("2025-01-01", periods=12, freq="ME"),
"revenue": [45000, 48000, 52000, 49000, 55000, 61000,
58000, 63000, 67000, 62000, 71000, 78000],
"expenses": [38000, 39000, 41000, 40000, 42000, 44000,
43000, 45000, 46000, 44000, 47000, 49000],
"region": ["East", "East", "East", "West", "West", "West",
"East", "East", "East", "West", "West", "West"],
})
sales = sales.set_index("month")
# Employee data
employees = pd.DataFrame({
"name": [f"Emp_{i}" for i in range(50)],
"department": np.random.choice(["Engineering", "Marketing", "Sales", "Support"], 50),
"salary": np.random.normal(75000, 15000, 50).astype(int),
"experience_years": np.random.uniform(1, 15, 50).round(1),
"performance_score": np.random.uniform(2.5, 5.0, 50).round(2),
})
Line plots
Line plots are the default. Best for time series and continuous data.
# Basic line plot
sales["revenue"].plot()
plt.title("Monthly Revenue")
plt.ylabel("Revenue ($)")
plt.show()
# Multiple lines
sales[["revenue", "expenses"]].plot()
plt.title("Revenue vs Expenses")
plt.ylabel("Amount ($)")
plt.show()
# Customized line plot
sales[["revenue", "expenses"]].plot(
figsize=(10, 6),
linewidth=2,
style=["o-", "s--"], # Circle markers solid, square markers dashed
color=["#2ecc71", "#e74c3c"],
)
plt.title("Revenue vs Expenses (2025)", fontsize=14)
plt.ylabel("Amount ($)", fontsize=12)
plt.xlabel("")
plt.legend(["Revenue", "Expenses"], fontsize=11)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Area plot
A filled line plot. Good for showing volume over time.
sales[["revenue", "expenses"]].plot.area(alpha=0.5, figsize=(10, 6))
plt.title("Revenue and Expenses Over Time")
plt.ylabel("Amount ($)")
plt.show()
# Stacked area (default)
sales[["revenue", "expenses"]].plot.area(stacked=True)
plt.show()
# Unstacked
sales[["revenue", "expenses"]].plot.area(stacked=False, alpha=0.4)
plt.show()
Bar plots
Bar plots compare discrete categories.
# Average salary by department
dept_salary = employees.groupby("department")["salary"].mean().sort_values()
dept_salary.plot.bar(figsize=(8, 5), color="steelblue")
plt.title("Average Salary by Department")
plt.ylabel("Salary ($)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Horizontal bar plot
Better when category labels are long.
dept_salary.plot.barh(figsize=(8, 5), color="steelblue")
plt.title("Average Salary by Department")
plt.xlabel("Salary ($)")
plt.tight_layout()
plt.show()
Grouped bar plot
# Revenue and expenses side by side
monthly_summary = sales[["revenue", "expenses"]].head(6)
monthly_summary.plot.bar(figsize=(10, 6), width=0.8)
plt.title("Monthly Revenue vs Expenses")
plt.ylabel("Amount ($)")
plt.xticks(rotation=45)
plt.legend(loc="upper left")
plt.tight_layout()
plt.show()
Stacked bar plot
monthly_summary.plot.bar(stacked=True, figsize=(10, 6))
plt.title("Revenue and Expenses (Stacked)")
plt.ylabel("Amount ($)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Histograms
Histograms show the distribution of a single numeric variable.
# Salary distribution
employees["salary"].plot.hist(bins=15, figsize=(8, 5), edgecolor="black")
plt.title("Salary Distribution")
plt.xlabel("Salary ($)")
plt.ylabel("Frequency")
plt.show()
# Compare distributions across departments
employees.groupby("department")["salary"].plot.hist(
alpha=0.5, bins=12, legend=True, figsize=(10, 6)
)
plt.title("Salary Distribution by Department")
plt.xlabel("Salary ($)")
plt.show()
# Multiple histograms in subplots
employees[["salary", "experience_years", "performance_score"]].plot.hist(
subplots=True,
layout=(1, 3),
figsize=(15, 4),
bins=12,
edgecolor="black",
)
plt.tight_layout()
plt.show()
KDE plot (smoothed distribution)
employees["salary"].plot.kde(figsize=(8, 5))
plt.title("Salary Density")
plt.xlabel("Salary ($)")
plt.show()
# Histogram with KDE overlay
ax = employees["salary"].plot.hist(bins=15, density=True, alpha=0.6, edgecolor="black")
employees["salary"].plot.kde(ax=ax, linewidth=2)
plt.title("Salary Distribution with KDE")
plt.xlabel("Salary ($)")
plt.show()
Scatter plots
Scatter plots show relationships between two numeric variables.
employees.plot.scatter(
x="experience_years",
y="salary",
figsize=(8, 6),
alpha=0.6,
)
plt.title("Experience vs Salary")
plt.xlabel("Years of Experience")
plt.ylabel("Salary ($)")
plt.show()
# Color by a third variable
employees.plot.scatter(
x="experience_years",
y="salary",
c="performance_score",
colormap="viridis",
figsize=(8, 6),
alpha=0.7,
)
plt.title("Experience vs Salary (colored by performance)")
plt.xlabel("Years of Experience")
plt.ylabel("Salary ($)")
plt.show()
# Size by a third variable
employees.plot.scatter(
x="experience_years",
y="salary",
s=employees["performance_score"] * 40, # Scale for visibility
alpha=0.5,
figsize=(8, 6),
)
plt.title("Experience vs Salary (sized by performance)")
plt.show()
Box plots
Box plots show the distribution summary: median, quartiles, and outliers.
# Single box plot
employees["salary"].plot.box(figsize=(6, 6))
plt.title("Salary Distribution")
plt.ylabel("Salary ($)")
plt.show()
# Box plot by category
employees.boxplot(column="salary", by="department", figsize=(10, 6))
plt.title("Salary Distribution by Department")
plt.suptitle("") # Remove the automatic title
plt.ylabel("Salary ($)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Multiple variables
employees[["salary", "experience_years"]].plot.box(
subplots=True,
layout=(1, 2),
figsize=(12, 5),
)
plt.tight_layout()
plt.show()
Pie charts
Pie charts show proportions. Use them sparingly and only with a few categories.
dept_counts = employees["department"].value_counts()
dept_counts.plot.pie(
autopct="%1.1f%%",
figsize=(8, 8),
startangle=90,
colors=["#3498db", "#2ecc71", "#e74c3c", "#f39c12"],
)
plt.title("Employee Distribution by Department")
plt.ylabel("") # Remove the default ylabel
plt.show()
Subplots
Multiple plots in a grid
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# Top left: line plot
sales[["revenue", "expenses"]].plot(ax=axes[0, 0])
axes[0, 0].set_title("Revenue vs Expenses")
# Top right: bar plot
dept_salary = employees.groupby("department")["salary"].mean()
dept_salary.plot.bar(ax=axes[0, 1], color="steelblue")
axes[0, 1].set_title("Avg Salary by Department")
axes[0, 1].tick_params(axis="x", rotation=45)
# Bottom left: histogram
employees["salary"].plot.hist(ax=axes[1, 0], bins=15, edgecolor="black")
axes[1, 0].set_title("Salary Distribution")
# Bottom right: scatter
employees.plot.scatter(
x="experience_years", y="salary", ax=axes[1, 1], alpha=0.5
)
axes[1, 1].set_title("Experience vs Salary")
plt.tight_layout()
plt.show()
Using the subplots parameter
# Pandas can create subplots automatically for each column
employees[["salary", "experience_years", "performance_score"]].plot(
subplots=True,
layout=(1, 3),
figsize=(15, 4),
sharex=False,
)
plt.tight_layout()
plt.show()
Customization reference
Colors
# Named colors
df.plot(color="steelblue")
# Hex colors
df.plot(color="#e74c3c")
# Color list for multiple series
df.plot(color=["#2ecc71", "#e74c3c", "#3498db"])
# Colormaps
df.plot(colormap="viridis") # Sequential
df.plot(colormap="Set2") # Categorical
df.plot(colormap="coolwarm") # Diverging
Figure size and DPI
df.plot(figsize=(12, 6)) # Width x Height in inches
# For saving high-resolution images
fig = df.plot(figsize=(12, 6)).get_figure()
fig.savefig("chart.png", dpi=150, bbox_inches="tight")
Axis formatting
ax = df.plot()
# Labels and title
ax.set_title("My Chart", fontsize=14, fontweight="bold")
ax.set_xlabel("X Axis", fontsize=12)
ax.set_ylabel("Y Axis", fontsize=12)
# Limits
ax.set_xlim(0, 100)
ax.set_ylim(0, 200000)
# Grid
ax.grid(True, alpha=0.3, linestyle="--")
# Format y-axis as currency
import matplotlib.ticker as mticker
ax.yaxis.set_major_formatter(mticker.StrMethodFormatter("${x:,.0f}"))
# Rotate tick labels
ax.tick_params(axis="x", rotation=45)
Legend
ax = df.plot()
ax.legend(loc="upper right", fontsize=10, framealpha=0.9)
# Move legend outside the plot
ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
Annotations
ax = sales["revenue"].plot(figsize=(10, 6))
# Annotate maximum value
max_idx = sales["revenue"].idxmax()
max_val = sales["revenue"].max()
ax.annotate(
f"Peak: ${max_val:,.0f}",
xy=(max_idx, max_val),
xytext=(max_idx - pd.Timedelta(days=60), max_val + 3000),
arrowprops=dict(arrowstyle="->", color="red"),
fontsize=11,
color="red",
)
plt.show()
When to use something else
Pandas plotting is great for quick exploratory analysis. For specific use cases, consider other tools.
Use Seaborn when you need statistical plots (violin plots, pair plots, heatmaps) or when you want attractive defaults with less customization code.
import seaborn as sns
sns.heatmap(employees[["salary", "experience_years", "performance_score"]].corr(),
annot=True, cmap="coolwarm")
Use Plotly when you need interactive charts (zoom, hover, pan) or when your charts will be embedded in a web application.
import plotly.express as px
fig = px.scatter(employees, x="experience_years", y="salary",
color="department", hover_data=["name"])
fig.show()
Use matplotlib directly when you need full control over every visual element, complex multi-panel figures, or custom geometries.
Wrapping Up
Pandas plotting covers 80% of your visualization needs during data analysis with minimal code. The .plot() method on DataFrames and Series creates line plots by default, and .plot.bar(), .plot.hist(), .plot.scatter(), and .plot.box() handle the other common chart types. For customization, grab the matplotlib axes object and use standard matplotlib methods. Save polished versions with fig.savefig(). For anything more complex - statistical visualizations, interactive charts, or custom layouts - graduate to Seaborn, Plotly, or raw matplotlib.
Related articles
- Pandas Pandas Cheat Sheet: 50 Essential Operations
A practical Pandas cheat sheet covering 50 essential DataFrame operations including selection, filtering, grouping, merging, and data cleaning with copy-paste examples.
- Pandas Pandas read_csv: Every Option Explained with Examples
Master pandas read_csv with practical examples for every important parameter including dtypes, parsing dates, handling missing data, chunked reading, and encoding.
- Pandas Pandas: apply vs Vectorization
When to reach for .apply and when vectorized operations win. A practical comparison with benchmarks, mental models, and the patterns that keep Pandas code both readable and fast.
- Pandas Pandas Categorical Data Tutorial
Use Pandas Categorical dtype to cut memory, speed up groupby, and encode ordered categories cleanly with practical conversion and pitfall notes.