PythonMastery
beginner 18 min read · lesson 5 of 6 in Data Science Fundamentals

Your First Data Visualisation

1 · The lesson

read

A well-chosen chart answers a question faster than any table, because people see patterns in shapes much more easily than in rows of numbers. Whether you are explaining a result to a manager or checking your own pipeline, a plot is the fastest way to understand what is happening in the data.

Matplotlib is the standard Python plotting library. Its code is long, but it is used almost everywhere, and other libraries such as seaborn are built on top of it. Once you know matplotlib, you can read the plotting code in most data science notebooks.

Note for the in-browser runner: matplotlib renders, but the image may not display in this Pyodide environment. Each example shows what the chart would look like in a comment.


1. The Three Charts You Will Use Most

Most business charts are one of these three:

ChartWhat it showsWhen to use it
BarOne value per categoryComparing groups: sales by region, errors by service
LineA value over an ordered axis (usually time)Trends, sequences, before/after
HistogramThe distribution of one numeric column"Is this column skewed? Bimodal? Outliers?"

Learn these three well before you use pie charts, radar charts or anything with three axes. Many impressive-looking charts you see online would be clearer as one of these three.


2. The Matplotlib Lifecycle

Every plot follows the same structure. Memorise it, and change only the middle line.

python
import matplotlib.pyplot as plt

plt.figure(figsize=(8, 4))            # 1. create a canvas
plt.plot([1, 2, 3, 4], [10, 25, 18, 30])   # 2. add data
plt.title("Sample Line Chart")        # 3. label
plt.xlabel("Quarter")
plt.ylabel("Revenue (£k)")
plt.grid(True, alpha=0.3)
plt.savefig("sample.png", dpi=150)    # 4. save (or plt.show())
plt.close()                           # 5. release the canvas

# Chart preview (text):
#  Revenue (£k)
#    30 |          *
#    25 |    *
#    18 |       *
#    10 | *
#       +------------------> Quarter
#        Q1   Q2  Q3  Q4

The convention is import matplotlib.pyplot as plt. The functional API (plt.plot, plt.title) is the easiest to learn. The object-oriented API (fig, ax = plt.subplots(); ax.plot(...)) is what you move to for complex figures.


3. Bar Chart: Categories

python
import matplotlib.pyplot as plt

categories = ["Coffee", "Tea", "Cocoa", "Juice"]
counts     = [420, 280, 95, 160]

plt.figure(figsize=(7, 4))
plt.bar(categories, counts, color="steelblue")
plt.title("Drinks Sold This Week")
plt.xlabel("Product")
plt.ylabel("Units")
plt.savefig("bars.png")
plt.close()

# Chart preview:
#  Units
#   420 | ███
#   280 |     ███
#   160 |             ███
#    95 |         ███
#       +-------------------
#        Coffee Tea Cocoa Juice

Use plt.barh for horizontal bars when category names are long. Always sort by value unless the categories have a natural order (months, days of week).


4. Line Chart: Time / Sequence

python
import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
sales  = [120, 135, 128, 160, 175, 190]

plt.figure(figsize=(7, 4))
plt.plot(months, sales, marker="o", linewidth=2)
plt.title("Monthly Sales, January to June 2026")
plt.xlabel("Month")
plt.ylabel("Sales (£k)")
plt.grid(True, alpha=0.3)
plt.savefig("line.png")
plt.close()

# Chart preview: an upward trend from 120 to 190 with markers on each point.

Line charts imply an ordered x-axis. Categories without order (Coffee, Tea, Cocoa) belong in a bar chart, never a line.


5. Histogram: Distribution

python
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
heights = rng.normal(loc=170, scale=8, size=500)        # 500 fake heights in cm

plt.figure(figsize=(7, 4))
plt.hist(heights, bins=20, color="seagreen", edgecolor="black")
plt.title("Distribution of Adult Heights")
plt.xlabel("Height (cm)")
plt.ylabel("Count")
plt.savefig("hist.png")
plt.close()

# Chart preview: a bell curve centred near 170, tails near 150 and 190.

The number of bins matters. Too few hides the shape, and too many shows random noise. bins=20 is a sensible default for sample sizes in the hundreds-to-thousands. Try bins="auto" and let matplotlib choose.


6. Subplots: Two Charts Side by Side

python
import matplotlib.pyplot as plt

categories = ["A", "B", "C", "D"]
counts     = [12, 35, 22, 18]
months     = ["Jan", "Feb", "Mar", "Apr"]
sales      = [100, 120, 95, 140]

fig, axes = plt.subplots(1, 2, figsize=(12, 4))    # 1 row, 2 columns

axes[0].bar(categories, counts, color="steelblue")
axes[0].set_title("Category Counts")
axes[0].set_xlabel("Category")
axes[0].set_ylabel("Count")

axes[1].plot(months, sales, marker="o", color="firebrick")
axes[1].set_title("Monthly Sales")
axes[1].set_xlabel("Month")
axes[1].set_ylabel("Sales")

fig.suptitle("Q1 Snapshot", fontsize=14)
plt.tight_layout()
plt.savefig("subplots.png")
plt.close()

# Chart preview: two charts side by side under one super-title.

When you switch to subplots, you also switch APIs. Note the axes[0].set_title(...) style instead of plt.title(...). This is the object-oriented API, which most professional code uses.


7. Scatter: Two Variables Together

python
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
hours = rng.uniform(0, 10, size=80)
scores = 40 + hours * 6 + rng.normal(0, 8, size=80)

plt.figure(figsize=(7, 5))
plt.scatter(hours, scores, alpha=0.7)
plt.title("Study Hours vs Exam Score")
plt.xlabel("Hours Studied")
plt.ylabel("Score")
plt.savefig("scatter.png")
plt.close()

# Chart preview: a noisy cloud of points that rises to the right (more hours, higher scores).

Scatter plots reveal relationships. If the points form a round cloud with no pattern, the variables are probably not related. If the cloud slopes up or down, they are correlated. Use scatter plots during EDA whenever you wonder "does X relate to Y?".


8. Seaborn: Matplotlib With Better Defaults

python
# import seaborn as sns
# import pandas as pd
#
# df = pd.DataFrame({
#     "city":   ["London", "Paris", "Berlin", "London", "Paris", "Berlin"],
#     "salary": [55_000, 72_000, 91_000, 38_000, 64_000, 88_000],
# })
#
# sns.boxplot(data=df, x="city", y="salary")
# plt.title("Salary by City")
# plt.savefig("seaborn_box.png")

Seaborn is built on top of matplotlib. It adds two things: better-looking defaults, and statistical chart types (boxplots, violin plots, regression plots, heatmaps) that each take one line. For exploring data, it is quicker to write than matplotlib. For fine control, you use matplotlib directly.


9. plt.show() vs plt.savefig()

  • In a Jupyter notebook, the chart renders inline automatically. You can call plt.show() to make it clear, but it is optional.
  • In a .py script, nothing displays unless you call plt.show() (opens a window) or plt.savefig("out.png") (writes a file).
  • In pipelines and CI, always savefig to a file path. show() stops and waits for a person to close the window.
python
plt.savefig("chart.png", dpi=150, bbox_inches="tight")
+ setup added so this can run · re-runs 6 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical
class _AutoMock:
    def __init__(self, name='mock'): self._name = name
    def __getattr__(self, k): return _AutoMock(self._name + '.' + k)
    def __call__(self, *a, **kw):
        print('-> ' + self._name + '() called')
        return _AutoMock(self._name + '()')
    def __repr__(self): return '<mock ' + self._name + '>'
    def __str__(self): return '<mock ' + self._name + '>'
    def __bool__(self): return True
    def __iter__(self): return iter([])
    def __len__(self): return 0
    def __getitem__(self, k): return _AutoMock(self._name + '[...]')
    def __setitem__(self, k, v): pass
    def __enter__(self): return self
    def __exit__(self, *a): return False
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    def __add__(self, o): return self
    def __radd__(self, o): return self
    def __sub__(self, o): return self
    def __mul__(self, o): return self
    def __rmul__(self, o): return self
    def __truediv__(self, o): return self
    def __eq__(self, o): return isinstance(o, _AutoMock)
    def __hash__(self): return hash(self._name)
    def __lt__(self, o): return True
    def __le__(self, o): return True
    def __gt__(self, o): return False
    def __ge__(self, o): return False
    def __mro_entries__(self, bases): return (object,)

plt = _AutoMock('plt')

# Earlier blocks on this page that this one builds on, re-run quietly first.
try:
    import matplotlib.pyplot as plt
    import numpy as np
except Exception:
    pass
_pm_earlier = [
    'import matplotlib.pyplot as plt\n\nplt.figure(figsize=(8, 4))            # 1. create a canvas\nplt.plot([1, 2, 3, 4], [10, 25, 18, 30])   # 2. add data\nplt.title("Sample Line Chart")        # 3. label\nplt.xlabel("Quarter")\nplt.ylabel("Revenue (£k)")\nplt.grid(True, alpha=0.3)\nplt.savefig("sample.png", dpi=150)    # 4. save (or plt.show())\nplt.close()                           # 5. release the canvas\n\n# Chart preview (text):\n#  Revenue (£k)\n#    30 |          *\n#    25 |    *\n#    18 |       *\n#    10 | *\n#       +------------------> Quarter\n#        Q1   Q2  Q3  Q4',
    'import matplotlib.pyplot as plt\n\ncategories = ["Coffee", "Tea", "Cocoa", "Juice"]\ncounts     = [420, 280, 95, 160]\n\nplt.figure(figsize=(7, 4))\nplt.bar(categories, counts, color="steelblue")\nplt.title("Drinks Sold This Week")\nplt.xlabel("Product")\nplt.ylabel("Units")\nplt.savefig("bars.png")\nplt.close()\n\n# Chart preview:\n#  Units\n#   420 | ███\n#   280 |     ███\n#   160 |             ███\n#    95 |         ███\n#       +-------------------\n#        Coffee Tea Cocoa Juice',
    'import matplotlib.pyplot as plt\n\nmonths = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]\nsales  = [120, 135, 128, 160, 175, 190]\n\nplt.figure(figsize=(7, 4))\nplt.plot(months, sales, marker="o", linewidth=2)\nplt.title("Monthly Sales, January to June 2026")\nplt.xlabel("Month")\nplt.ylabel("Sales (£k)")\nplt.grid(True, alpha=0.3)\nplt.savefig("line.png")\nplt.close()\n\n# Chart preview: an upward trend from 120 to 190 with markers on each point.',
    'import matplotlib.pyplot as plt\nimport numpy as np\n\nrng = np.random.default_rng(42)\nheights = rng.normal(loc=170, scale=8, size=500)        # 500 fake heights in cm\n\nplt.figure(figsize=(7, 4))\nplt.hist(heights, bins=20, color="seagreen", edgecolor="black")\nplt.title("Distribution of Adult Heights")\nplt.xlabel("Height (cm)")\nplt.ylabel("Count")\nplt.savefig("hist.png")\nplt.close()\n\n# Chart preview: a bell curve centred near 170, tails near 150 and 190.',
    'import matplotlib.pyplot as plt\n\ncategories = ["A", "B", "C", "D"]\ncounts     = [12, 35, 22, 18]\nmonths     = ["Jan", "Feb", "Mar", "Apr"]\nsales      = [100, 120, 95, 140]\n\nfig, axes = plt.subplots(1, 2, figsize=(12, 4))    # 1 row, 2 columns\n\naxes[0].bar(categories, counts, color="steelblue")\naxes[0].set_title("Category Counts")\naxes[0].set_xlabel("Category")\naxes[0].set_ylabel("Count")\n\naxes[1].plot(months, sales, marker="o", color="firebrick")\naxes[1].set_title("Monthly Sales")\naxes[1].set_xlabel("Month")\naxes[1].set_ylabel("Sales")\n\nfig.suptitle("Q1 Snapshot", fontsize=14)\nplt.tight_layout()\nplt.savefig("subplots.png")\nplt.close()\n\n# Chart preview: two charts side by side under one super-title.',
    'import matplotlib.pyplot as plt\nimport numpy as np\n\nrng = np.random.default_rng(0)\nhours = rng.uniform(0, 10, size=80)\nscores = 40 + hours * 6 + rng.normal(0, 8, size=80)\n\nplt.figure(figsize=(7, 5))\nplt.scatter(hours, scores, alpha=0.7)\nplt.title("Study Hours vs Exam Score")\nplt.xlabel("Hours Studied")\nplt.ylabel("Score")\nplt.savefig("scatter.png")\nplt.close()\n\n# Chart preview: a noisy cloud of points that rises to the right (more hours, higher scores).',
]
def _pm_run_earlier(_srcs, _g):
    import sys as _s, io as _io, warnings as _w
    for _src in _srcs:
        _o, _e = _s.stdout, _s.stderr
        _s.stdout = _s.stderr = _io.StringIO()
        try:
            with _w.catch_warnings():
                _w.simplefilter('ignore')
                exec(compile(_src, '<earlier block>', 'exec'), _g)
        except (Exception, SystemExit):
            pass
        finally:
            _s.stdout, _s.stderr = _o, _e
    if 'matplotlib.pyplot' in _s.modules:
        _s.modules['matplotlib.pyplot'].close('all')   # their plots belong to their own blocks
_pm_run_earlier(_pm_earlier, globals())
del _pm_earlier, _pm_run_earlier

dpi=150 gives a sharp image, and bbox_inches="tight" trims surrounding whitespace.


10. Chart Design: A Short Style Guide

A chart is for the reader, not for the person who made it. Five rules:

1. Title states the main finding in plain English. Not "Bar Chart", but "Coffee outsells tea 1.5× since March".
2. Axis labels include units. Revenue (£k), not just Revenue.
3. No chartjunk: remove 3D effects, gradients, drop shadows and decorative icons. Edward Tufte called the share of ink that shows data the "data-ink ratio". Keep it high.
4. Pick a colour-blind safe palette. viridis, cividis, and seaborn's defaults are designed for it. Don't use red and green as the only way to tell two things apart.
5. Pick the right chart. Pie charts work only for 2-5 categories that add up to a meaningful whole. Most pie charts would be clearer as bar charts.


Common Mistakes

  • No axis labels. A reader shouldn't have to read code to know what they're looking at.
  • Pie chart with 12 slices. Use a sorted bar chart instead.
  • Line chart over unordered categories. A line suggests that the x-axis has an order. "Apples → Bananas → Cherries" has no order.
  • Forgetting plt.close() in scripts. Each plt.figure() consumes memory until the figure is closed.
  • Ignoring colour-blind readers. Check your chart with a colour-blindness simulator before you share it.
  • Saving with the default DPI. 100 dpi looks blurry in slides, so use 150 or 200.

🎯 Your Turn: A Labelled Bar Chart

Given monthly sales numbers, produce a clean bar chart and save it to sales.png. Include a title, axis labels, and units.

Skeleton:

python
import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May",
          "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
sales  = [100, 150, 90, 140, 180, 200,
          230, 210, 175, 160, 140, 220]

def plot_monthly_sales(months, sales, out_path="sales.png"):
    # TODO 1: create a figure with a sensible size
    # TODO 2: plt.bar(months, sales)
    # TODO 3: title, xlabel, ylabel, grid
    # TODO 4: savefig with dpi=150, bbox_inches="tight"
    # TODO 5: plt.close() to release the figure
    ...

plot_monthly_sales(months, sales)
Hint 1: The 4-line core You need plt.figure(figsize=(10, 4)), plt.bar(months, sales), your three labels, and plt.savefig(out_path, dpi=150, bbox_inches="tight"). Anything more is optional.
Hint 2: A title that states the finding "Monthly Sales 2026" works. "Sales climbed 130% Jan→Jul, then fell until November" works better. Use the finding as the title, not the chart type.
Show full solution
python
import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May",
          "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
sales  = [100, 150, 90, 140, 180, 200,
          230, 210, 175, 160, 140, 220]

def plot_monthly_sales(months, sales, out_path="sales.png"):
    plt.figure(figsize=(10, 4))
    plt.bar(months, sales, color="steelblue", edgecolor="black")
    plt.title("Monthly Sales 2026")
    plt.xlabel("Month")
    plt.ylabel("Sales (units)")
    plt.grid(True, axis="y", alpha=0.3)
    plt.savefig(out_path, dpi=150, bbox_inches="tight")
    plt.close()

plot_monthly_sales(months, sales)
print("Saved sales.png")

# Chart preview: 12 vertical blue bars, lowest in March, highest in July.

Notice that the grid is only on the y-axis. Vertical gridlines on a categorical x-axis add nothing, so leaving them out makes the chart easier to read.


What You Learned

  • The three most-used charts are bar (categories), line (sequence) and histogram (distribution).
  • Every matplotlib plot follows the same figure → plot → label → save lifecycle.
  • Subplots put multiple charts in one figure, using the object-oriented API.
  • Scatter plots reveal relationships between two numeric variables.
  • Seaborn adds better-looking defaults and statistical chart types on top of matplotlib.
  • A good chart is one a reader understands without reading the code: put the finding in the title, label the axes, and remove the chartjunk.

Next: Exploratory Data Analysis: combine pandas and plotting into a step-by-step way to look at your data.

Found this useful? Share it with a friend who’s learning Python.