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
"""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() * 100turns 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/ax2viatwinx()) 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.
--toptruncates 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.