Matplotlib Scatter Plots in Python — Complete Guide
Learn to create and customize scatter plots in Python with Matplotlib. Covers color, size, alpha, colorbars, multi-group plots, and annotations.
Matplotlib's scatter() function lets you visualize the relationship between two numerical variables by placing a marker at every (x, y) data point. Unlike a line plot, scatter plots make no assumption about order or continuity — each point stands on its own. This makes them the go-to choice for exploring correlations, spotting clusters, and detecting outliers.
This chapter covers everything from your first scatter plot to professional techniques: per-point color and size encoding, transparency, colorbars, multi-group charts, point annotations, and saving publication-ready files.
Before diving in, make sure Matplotlib is installed:
pip install matplotlibIf you are new to Matplotlib, read the Matplotlib Introduction and Getting Started chapters first.
When to Use a Scatter Plot
Use a scatter plot when:
- You want to explore the correlation between two numerical variables (height vs. weight, hours studied vs. exam score).
- You need to detect clusters — groups of points that naturally clump together.
- You want to identify outliers — points far from the main distribution.
- You are encoding a third variable through marker size or color (a "bubble chart" is a scatter plot where size = a third variable).
Avoid scatter plots when one axis represents unordered categories — a bar chart is clearer there. For trends over a continuous ordered variable, a line plot is more appropriate.
Creating a Basic Scatter Plot
Pass two equal-length sequences — x values and y values — to plt.scatter():
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
y = [2, 4, 5, 4, 7, 8, 6, 9, 10, 12]
plt.scatter(x, y)
plt.title('Basic Scatter Plot')
plt.xlabel('X Values')
plt.ylabel('Y Values')
plt.tight_layout()
plt.show()plt.tight_layout() prevents labels from being clipped — make it a habit before every show() or savefig() call.
Customizing Marker Size
The s parameter controls marker size in points squared (default is 20). Increase it to make points easier to see, especially in presentations:
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5, 6, 7, 8]
y = [3, 1, 4, 1, 5, 9, 2, 6]
plt.scatter(x, y, s=120)
plt.title('Larger Markers')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()You can also pass a list or array to s so each point gets its own size — this is how you encode a third numerical variable as bubble size:
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [5, 3, 6, 2, 7]
sizes = [100, 300, 50, 400, 200] # third variable encoded as bubble area
plt.scatter(x, y, s=sizes)
plt.title('Bubble Chart — size encodes a third variable')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()Customizing Marker Color
A Single Color for All Points
Pass any color name, hex string, or RGB tuple to c (or color):
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [2, 4, 1, 5, 3]
plt.scatter(x, y, s=100, c='steelblue')
plt.title('Single Color')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()Per-Point Color from a Numeric Variable
Passing an array to c maps each value to a color via the colormap specified in cmap. Add plt.colorbar() to show what the colors represent:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=42)
x = rng.random(50)
y = rng.random(50)
values = rng.random(50) # third variable, e.g. intensity or temperature
scatter = plt.scatter(x, y, s=80, c=values, cmap='viridis')
plt.colorbar(scatter, label='Intensity')
plt.title('Color-Mapped Scatter Plot')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()'viridis' is a perceptually uniform colormap that is readable in greyscale and accessible to color-blind readers. Other good choices are 'plasma', 'cividis', and 'coolwarm'.
Controlling Transparency with alpha
When many points overlap, they form an opaque blob that hides the true density. Set alpha (0 = fully transparent, 1 = fully opaque) to reveal overlapping structure:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=0)
x = rng.normal(loc=0, scale=1, size=300)
y = rng.normal(loc=0, scale=1, size=300)
plt.scatter(x, y, s=40, alpha=0.4)
plt.title('Transparent Markers Reveal Density (alpha=0.4)')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()A good starting point is alpha=0.4 to 0.6. Adjust based on how many points you have.
Styling Marker Edges
Use edgecolors to add a border around each marker, and linewidths to control the border thickness. This helps points stand out against a colored background:
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5, 6]
y = [3, 1, 4, 1, 5, 9]
plt.scatter(x, y, s=150, c='gold', edgecolors='black', linewidths=1.5)
plt.title('Markers with Edges')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()Pass edgecolors='none' to remove edges entirely (this is the default for most colormaps).
Plotting Multiple Groups
To compare groups, call plt.scatter() once per group and assign a label. Matplotlib assigns a different default color to each call:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=7)
# Group A — centered around (2, 3)
ax_x = rng.normal(loc=2, scale=0.5, size=30)
ax_y = rng.normal(loc=3, scale=0.5, size=30)
# Group B — centered around (5, 6)
bx_x = rng.normal(loc=5, scale=0.5, size=30)
bx_y = rng.normal(loc=6, scale=0.5, size=30)
# Group C — centered around (8, 2)
cx_x = rng.normal(loc=8, scale=0.5, size=30)
cx_y = rng.normal(loc=2, scale=0.5, size=30)
plt.scatter(ax_x, ax_y, s=60, label='Group A')
plt.scatter(bx_x, bx_y, s=60, label='Group B')
plt.scatter(cx_x, cx_y, s=60, label='Group C')
plt.legend()
plt.title('Multi-Group Scatter Plot')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()Each scatter() call automatically picks the next color in the default color cycle. Pass c='red' (or any color) to override.
Annotating Individual Points
Use plt.annotate() to label specific points — useful for highlighting outliers or key observations:
import matplotlib.pyplot as plt
cities = ['London', 'Berlin', 'Madrid', 'Rome', 'Paris']
population = [9.0, 3.7, 3.3, 2.8, 2.1] # millions
area = [1572, 892, 604, 1285, 105] # km²
plt.scatter(area, population, s=100, c='steelblue', edgecolors='black', linewidths=0.8)
for i, city in enumerate(cities):
plt.annotate(
city,
xy=(area[i], population[i]),
xytext=(8, 4), # offset in points
textcoords='offset points',
fontsize=9,
)
plt.title('European City Population vs. Area')
plt.xlabel('Area (km²)')
plt.ylabel('Population (millions)')
plt.tight_layout()
plt.show()The xytext + textcoords='offset points' pattern shifts the label slightly so it does not sit directly on top of the marker.
Using Logarithmic Axes
When data spans several orders of magnitude, linear axes compress most points into a corner. Switch to a log scale with plt.xscale('log') or plt.yscale('log'):
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=1)
x = np.logspace(1, 5, 60) # 10¹ to 10⁵
y = x * rng.uniform(0.5, 2.0, 60) # roughly proportional, with noise
plt.scatter(x, y, s=40, alpha=0.7)
plt.xscale('log')
plt.yscale('log')
plt.title('Log-Scale Scatter Plot')
plt.xlabel('X (log scale)')
plt.ylabel('Y (log scale)')
plt.tight_layout()
plt.show()Both axes now span even intervals of powers of ten, spreading the data evenly across the plot area.
Adding a Regression Line
A scatter plot shows individual points; adding a line of best fit shows the overall trend. Compute slope and intercept with np.polyfit():
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=3)
x = np.linspace(0, 10, 40)
y = 2.5 * x + rng.normal(scale=3, size=40) # linear trend + noise
# Fit a degree-1 polynomial (straight line)
slope, intercept = np.polyfit(x, y, 1)
trend_line = slope * x + intercept
plt.scatter(x, y, s=50, label='Data points', alpha=0.7)
plt.plot(x, trend_line, color='red', linewidth=2, label=f'Trend (slope={slope:.2f})')
plt.legend()
plt.title('Scatter Plot with Regression Line')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.show()np.polyfit(x, y, 1) returns [slope, intercept] for the best-fit line through the points.
Saving a Scatter Plot to a File
Use plt.savefig() instead of plt.show() to write the chart to disk. Call it before plt.show() — after show() the figure is cleared:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=9)
x = rng.random(60)
y = rng.random(60)
plt.scatter(x, y, s=60, alpha=0.6, c='teal', edgecolors='white', linewidths=0.5)
plt.title('Saved Scatter Plot')
plt.xlabel('X')
plt.ylabel('Y')
plt.tight_layout()
plt.savefig('scatter.png', dpi=150) # PNG at 150 DPI
plt.savefig('scatter.pdf') # vector PDF — best for publication
plt.show()Common format options: 'png' (raster, web), 'pdf' (vector, publication), 'svg' (vector, web). Raise dpi to 300 for print-quality rasters.
scatter() vs plot() — Which to Use?
Both functions can draw marker-only plots, but they serve different purposes:
| Feature | plt.scatter() | plt.plot() |
|---|---|---|
Per-point size (s) | Yes — pass an array | No |
Per-point color (c) | Yes — pass an array | No (one color per call) |
| Colormap support | Yes (cmap) | Limited |
| Performance on large datasets | Slower | Faster |
| Connecting line | No | Yes |
Use scatter() when you need per-point styling (color or size varies by data). Use plot(marker='o', linestyle='None') for simple marker plots on large datasets where speed matters. See the Matplotlib Markers chapter for more on marker styles.
Complete Example
The following self-contained script combines the most useful techniques — color mapping, size encoding, transparency, a colorbar, and a legend:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(seed=42)
n = 80
x = rng.standard_normal(n)
y = 0.8 * x + rng.standard_normal(n) * 0.6 # correlated
sizes = rng.uniform(30, 200, n) # bubble area
values = rng.random(n) # third variable for color
fig, ax = plt.subplots(figsize=(8, 5))
sc = ax.scatter(
x, y,
s=sizes,
c=values,
cmap='plasma',
alpha=0.75,
edgecolors='white',
linewidths=0.5,
)
plt.colorbar(sc, ax=ax, label='Intensity')
ax.set_title('Comprehensive Scatter Plot Example', fontsize=13)
ax.set_xlabel('X Variable')
ax.set_ylabel('Y Variable (correlated)')
fig.tight_layout()
plt.savefig('scatter_complete.png', dpi=150)
plt.show()Key points:
fig, ax = plt.subplots()gives you explicit figure and axes objects — the recommended approach for anything beyond a quick prototype.ax.scatter()on anAxesobject behaves identically toplt.scatter().plt.colorbar(sc, ax=ax, label='...')attaches the colorbar to the specific axes.
Best Practices
- Show the scale. If you use
cwith a colormap, always add a colorbar so readers know what the colors mean. - Avoid overplotting. For more than ~500 points, set
alpha < 1or switch to a 2-D histogram (plt.hist2d()) or a hexbin plot (plt.hexbin()). - Choose accessible colormaps.
'viridis','plasma', and'cividis'are perceptually uniform and color-blind friendly. Avoid'jet'and'rainbow'. - Label axes with units.
plt.xlabel('Height (cm)')is more informative thanplt.xlabel('Height'). - Add a title that states the finding. "Height vs. Weight — positive correlation" is more useful than "Scatter Plot".
Related Chapters
- Matplotlib Introduction — library overview and installation
- Getting Started with Matplotlib — your first figures
- Matplotlib Line Plots — trends over continuous variables
- Matplotlib Bar Charts — comparing discrete categories
- Matplotlib Histograms — distribution of a single variable
- Matplotlib Markers — every marker style and customization option
- Matplotlib Subplots — combining multiple charts in one figure