Code Library
Shop DataPythonPython 3.8+ · pandas · matplotlibMIT licenseIntermediate

Downtime Pareto chart from a CSV (pandas + matplotlib)

Every downtime meeting eventually asks the same question: which handful of reasons account for most of the lost time? This script answers it in three lines of pandas — group by reason, sum the minutes, sort — then plots the classic Pareto chart: bars for total minutes, a cumulative-percentage line, and the 80% reference line that tells you how many reasons actually matter.

Before you run it

  • pip install pandas matplotlib
  • A downtime log CSV with columns reason, minutes (extra columns ignored)

No data of your own yet?

The 15-row downtime log used above — run the script against it and get the exact chart and numbers shown.

The code

GitHub
"""Plot a downtime Pareto chart from a CSV of downtime events.

CSV needs the columns: reason, minutes  (extra columns ignored).

Usage:  python downtime_pareto.py downtime_log.csv --out pareto.png
"""

import argparse

import pandas as pd
import matplotlib.pyplot as plt


def main():
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument("csv_file", help="downtime log CSV: reason, minutes")
    ap.add_argument("--out", default="pareto.png")
    ap.add_argument("--top", type=int, default=10, help="show only the top N reasons")
    args = ap.parse_args()

    df = pd.read_csv(args.csv_file)
    totals = df.groupby("reason")["minutes"].sum().sort_values(ascending=False)
    totals = totals.head(args.top)

    cum_pct = totals.cumsum() / totals.sum() * 100

    fig, ax1 = plt.subplots(figsize=(9, 5.5))
    ax1.bar(totals.index, totals.values, color="#b45309")
    ax1.set_ylabel("Downtime (minutes)")
    ax1.set_xticks(range(len(totals)))
    ax1.set_xticklabels(totals.index, rotation=30, ha="right")

    ax2 = ax1.twinx()
    ax2.plot(range(len(totals)), cum_pct.values, "o-", color="#1d4ed8")
    ax2.axhline(80, color="#dc2626", linestyle="--", linewidth=1)
    ax2.set_ylabel("Cumulative %")
    ax2.set_ylim(0, 105)

    fig.suptitle("Downtime Pareto")
    fig.tight_layout()
    fig.savefig(args.out, dpi=150)

    print(f"Top reason: {totals.index[0]} ({totals.iloc[0]:.0f} min, "
          f"{cum_pct.iloc[0]:.1f}% of total)")
    n80 = (cum_pct <= 80).sum() + 1
    print(f"{n80} reason(s) account for 80% of downtime.")
    print(f"Chart saved to {args.out}")


if __name__ == "__main__":
    main()

What you get

$ python downtime_pareto.py downtime_log.csv --out pareto.png
Top reason: Setup/changeover (420 min, 35.0% of total)
3 reason(s) account for 80% of downtime.
Chart saved to pareto.png

How it works

  • groupby("reason")["minutes"].sum().sort_values(ascending=False) is the entire analysis — pandas does the aggregation and ranking in one line, which is most of why this beats a hand-rolled dictionary-counting version.
  • cumsum() / sum() * 100 turns the sorted totals into the cumulative-percentage line every Pareto chart needs — the 80% rule (a handful of causes account for most of the downtime) is exactly what that line is designed to show.
  • Two y-axes (ax1/ax2 via twinx()) let bars (minutes) and the cumulative line (percent) share one chart without one scale crushing the other.
  • The 80% reference line isn't decoration — counting how many bars fall left of where the cumulative line crosses it is literally how you read a Pareto chart.

Gotchas & honest limits

  • Category names must be spelled identically to group correctly — "Tool change" and "tool change" become two different bars. Normalize your downtime log's reason field before charting.
  • --top truncates to the top N reasons by default (10) — if your real log has dozens of reasons, check that truncation isn't hiding a moderate contributor worth fixing.
  • This charts duration, not frequency — a reason that happens rarely but for a long time each time outranks one that happens constantly but briefly. Decide which one your shop actually needs to reduce.
  • Minutes are summed as given — mixing units (some rows in minutes, some accidentally in seconds) will silently produce a wrong, confident-looking chart.

Goes deeper

Want this adapted to your shop — or built into a real tool?

Samples are the free 80%. The last 20% is the part I do for a living.

Get in touch
Home
Blog
Tools
Code
Email
LinkedIn
Résumé