1. はじめに — なぜ Excel ではなく Python なのか

品質管理(QC)の現場には、Excel とその上に組まれたマクロ・関数群で動いている資産が山のようにあります。多くのケースで Excel は「動いている」のですが、次のような限界に直面します。

  • 1 ファイルあたり数万行を超えると動作が極端に重くなる
  • 異なるロット・装置・ラインのデータを横断集計するのが面倒
  • 担当者が変わると「Excel 内のロジック」がブラックボックス化する
  • 同じレポートを毎月手作業で作る、しかも誰かが手作業のミスをする
  • X̄-R 管理図や Cp/Cpk のような「定義通りの計算」を実装し直すたびに微妙にズレる

これらの大半は、Python + pandas + Matplotlib に移行すれば解決します。本記事では、QC の現場で最頻出のグラフ類を、再現可能なコードで実装していきます。

2. 環境構築とサンプルデータ

2.1 必要ライブラリ

python -m pip install pandas matplotlib numpy streamlit

コマンドプロンプトまたは PowerShell で実行します(venv の作成やプロキシ環境での対処は 学習ロードマップ STEP 2 を参照)。numpypandas と一緒に入りますが、明示しておきます。streamlit は最後のダッシュボード化セクションで使います。

2.2 サンプルデータの生成

業務データは持ち出せないため、本記事では仮想的な品質測定データを生成して使います。「ロット ID、装置、測定値、測定時刻」という典型的な構造です。

# gen_sample.py — 自宅検証用のダミーデータ生成
import numpy as np
import pandas as pd

rng = np.random.default_rng(42)

n_lots = 50
n_per_lot = 5
machines = ["M-01", "M-02", "M-03"]

records = []
for lot in range(1, n_lots + 1):
    machine = rng.choice(machines)
    # ロットごとに微妙にバイアス、装置ごとにわずかな差をつける
    bias = rng.normal(0, 0.5)
    machine_offset = {"M-01": 0.0, "M-02": 0.1, "M-03": -0.1}[machine]
    for i in range(n_per_lot):
        value = 100.0 + bias + machine_offset + rng.normal(0, 0.8)
        records.append({
            "lot": f"L{lot:04d}",
            "machine": machine,
            "measured_at": pd.Timestamp("2026-04-01") + pd.Timedelta(hours=lot, minutes=i * 5),
            "value": value,
        })

df = pd.DataFrame(records)
df.to_csv("samples.csv", index=False, encoding="utf-8-sig")
print(df.head())
print(f"rows: {len(df)}")

スペック: 規格中心値 100、規格上限 102、規格下限 98(つまりトレランス±2)の製品を想定しています。以降のコードはこの samples.csv を使います。

3. pandas で集計する

3.1 基本集計

import pandas as pd

df = pd.read_csv("samples.csv", parse_dates=["measured_at"])

# 全体統計
print(df["value"].describe())

# 装置別の平均と標準偏差
by_machine = df.groupby("machine")["value"].agg(["mean", "std", "count"])
print(by_machine)

# ロット平均
lot_mean = df.groupby("lot")["value"].agg(["mean", "std"]).reset_index()
print(lot_mean.head())

これらの集計を Excel で同じことをやろうとすると、ピボットテーブルを作って、グループ化して、関数を貼って——という作業に 30 分かかります。pandas なら 10 秒です。

3.2 規格外(NG)品の抽出

USL = 102.0   # 上限規格
LSL = 98.0    # 下限規格

ng = df[(df["value"] > USL) | (df["value"] < LSL)]
print(f"NG count: {len(ng)} / {len(df)}")
print(ng.groupby("machine").size())

2.2 のサンプルデータでは NG count: 10 / 250 と表示されます(筆者環境で実測)。「装置別の不良率はどれくらいか」が一行で出てきます。Excel で長年やってきた集計の大半は、こうやって 1〜2 行に圧縮できます。

4. Matplotlib の日本語対応

本題に入る前に、Matplotlib で日本語が「□」になる問題を解決しておきます。Windows ではフォント指定だけで通りますが、最も移植性が高いのは matplotlib-fontja パッケージを使う方法です(かつて定番だった japanize-matplotlib は 2020 年で更新が止まり、Python 3.12 以降では動作しません。matplotlib-fontja はその後継フォークで、使い方は同じです)。

python -m pip install matplotlib-fontja
import matplotlib_fontja  # noqa: F401  import するだけで日本語対応が有効化
import matplotlib.pyplot as plt

Windows のみで動かすなら、フォントを直接指定する書き方でも構いません。

import matplotlib.pyplot as plt

plt.rcParams["font.family"] = "Yu Gothic"  # または "Meiryo"
plt.rcParams["axes.unicode_minus"] = False  # マイナス記号の文字化け対策

フォント指定方式を選んだ場合は、以降のコードに出てくる import matplotlib_fontja の行を、この rcParams 2 行に読み替えてください(プロキシ・閉域網で pip が使えない現場 PC では、フォント指定方式のほうが確実です)。

5. ヒストグラムで分布を見る

品質管理で最初に見るべきは「分布の形」です。正規分布から大きくずれていないか、規格中心からの偏りはないか、複数の山が見えるかを確認します。

# qc_charts.py — 本記事の関数はこのファイルにまとめていきます
import matplotlib_fontja  # noqa: F401
import matplotlib.pyplot as plt
import pandas as pd

USL, LSL = 102.0, 98.0
TARGET = 100.0


def save_histogram(df: pd.DataFrame, path,
                   usl: float = USL, lsl: float = LSL,
                   target: float = TARGET) -> None:
    """測定値のヒストグラムを規格線付きで PNG 保存する。"""
    fig, ax = plt.subplots(figsize=(8, 4.5))
    ax.hist(df["value"], bins=30, edgecolor="black", alpha=0.8)
    ax.axvline(target, color="green", linestyle="-", label=f"中心 {target}")
    ax.axvline(usl, color="red", linestyle="--", label=f"上限規格 {usl}")
    ax.axvline(lsl, color="red", linestyle="--", label=f"下限規格 {lsl}")
    ax.set_xlabel("測定値")
    ax.set_ylabel("度数")
    ax.set_title("測定値のヒストグラム")
    ax.legend()
    fig.tight_layout()
    fig.savefig(path, dpi=150)
    plt.close(fig)  # 図を明示的に閉じる(繰り返し実行時のメモリ対策)


if __name__ == "__main__":
    df = pd.read_csv("samples.csv")
    save_histogram(df, "hist.png")

実行するとグラフは画面には表示されず、同じフォルダに hist.png として保存されます(その場で見たい場合は fig.savefig(...) の後に plt.show() を足してください)。規格線をグラフに重ねるだけで、「中心からどれくらいずれているか」「規格内にどれくらい収まっているか」が直感的に把握できます。関数の形で書いているのは、9 章の月次レポート自動化でそのまま再利用するためです。

6. Cp / Cpk の計算

工程能力指数(Cp / Cpk)は、工程が規格に対してどれだけ余裕を持って製造できているかを示す代表的な指標です。定義は次の通りです。

  • Cp = (USL − LSL) / (6σ)
  • Cpk = min((USL − μ) / (3σ), (μ − LSL) / (3σ))

Cp は「分布の幅」だけを見ますが、Cpk は「中心ずれ」も加味します。一般的に Cpk >= 1.33 が「工程能力あり」、Cpk >= 1.67 が「余裕あり」の目安とされます。この数値は自動車業界の量産部品承認プロセス AIAG PPAP(第 4 版)の受入基準——1.67 超で合格、1.33〜1.67 は顧客と要協議、1.33 未満は不合格——に由来するもので、業界・顧客によって要求値は変わります。

# qc_charts.py に追記(import pandas は 5 章で済んでいるので再掲不要)
def cp_cpk(values: pd.Series, usl: float, lsl: float) -> dict:
    mu = values.mean()
    sigma = values.std(ddof=1)
    cp = (usl - lsl) / (6 * sigma)
    cpu = (usl - mu) / (3 * sigma)
    cpl = (mu - lsl) / (3 * sigma)
    cpk = min(cpu, cpl)
    return {"mean": mu, "std": sigma, "Cp": cp, "Cpk": cpk, "CPU": cpu, "CPL": cpl}


if __name__ == "__main__":  # import 時に実行されないようガード(9 章で import するため)
    df = pd.read_csv("samples.csv")
    print(cp_cpk(df["value"], usl=102.0, lsl=98.0))

    # 装置別
    for m, group in df.groupby("machine"):
        stats = cp_cpk(group["value"], usl=102.0, lsl=98.0)
        print(f"{m}: Cp={stats['Cp']:.3f}, Cpk={stats['Cpk']:.3f}")

「装置別の Cpk が 1.33 を割っている」と分かれば、その装置の点検・調整を優先するという意思決定につながります。

1 つ、品質保証部門に見せる前に知っておくべき注意があります。本記事の cp_cpk() は簡便のため全体標準偏差std(ddof=1))で計算しています。AIAG(米国自動車産業)流の呼び分けでは、全体標準偏差で計算する指標は Pp / Ppk(工程性能指数)と呼ばれ、Cp / Cpk は群内標準偏差 σ̂ = R̄ / d₂(サブグループサイズ n=5 のとき d₂ = 2.326)で計算するのが伝統的な定義です。日本の現場では全体標準偏差で Cp を計算する流儀も広く使われていますが、どちらの σ を使ったかは必ず明記してください——「Excel の値と合わない」原因の定番でもあります。

7. X̄-R 管理図

X̄-R 管理図は、ロットごとの平均(X̄)と範囲(R = max − min)を時系列で並べ、管理限界線とともに表示するグラフです。日本の QC 現場でもっとも一般的な統計的工程管理(SPC)ツールの 1 つです。

7.1 管理限界の計算

管理限界は、サブグループサイズ n ごとに決まる係数 A2, D3, D4 を使って次のように計算します。

  • X̄ 図 UCL = X̄̄ + A2 × R̄
  • X̄ 図 LCL = X̄̄ − A2 × R̄
  • R 図 UCL = D4 × R̄
  • R 図 LCL = D3 × R̄
# qc_charts.py に追記
# サブグループサイズ n=5 のときの係数
A2 = 0.577
D3 = 0.0
D4 = 2.114


def xbar_r_chart_data(df: pd.DataFrame, group_col: str, value_col: str) -> pd.DataFrame:
    grouped = df.groupby(group_col)[value_col].agg(["mean", "min", "max"])
    grouped["R"] = grouped["max"] - grouped["min"]
    return grouped.rename(columns={"mean": "Xbar"})

A2・D3・D4 などの係数は、管理図の現行規格 ISO 7870-2 / JIS Z 9020-2 の係数表に規定されています(かつての ISO 8258 は廃止され、ISO 7870-2 に置き換えられました)。補足: n ≦ 6 では D3 が定義されないため、R 図の下方管理限界(LCL)は「設けない」(0 扱い)とするのが QC の作法です。本記事のコードで LCL=0 の線が引かれるのはそのためで、品証部門に見せるグラフでは LCL の線自体を省いても構いません。

7.2 描画

7.1 と同じ qc_charts.py に続けて書きます。凡例のラベルは、合成文字(X̄ のようなマクロン付き文字)がフォントによって崩れることがあるため、グラフ内では「総平均」「ロット平均」のような日本語表記にしています。

# qc_charts.py に追記
def save_xbar_r(df: pd.DataFrame, path) -> None:
    """Xbar-R 管理図を PNG 保存する。"""
    chart = xbar_r_chart_data(df, group_col="lot", value_col="value")

    xbar_mean = chart["Xbar"].mean()
    r_mean = chart["R"].mean()

    ucl_x = xbar_mean + A2 * r_mean
    lcl_x = xbar_mean - A2 * r_mean
    ucl_r = D4 * r_mean
    lcl_r = D3 * r_mean

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)

    # Xbar 図
    ax1.plot(chart.index, chart["Xbar"], marker="o", color="steelblue")
    ax1.axhline(xbar_mean, color="green", linestyle="-", label=f"総平均={xbar_mean:.2f}")
    ax1.axhline(ucl_x, color="red", linestyle="--", label=f"UCL={ucl_x:.2f}")
    ax1.axhline(lcl_x, color="red", linestyle="--", label=f"LCL={lcl_x:.2f}")
    ax1.set_ylabel("ロット平均")
    ax1.set_title("Xbar-R 管理図")
    ax1.legend(loc="upper right", fontsize=9)

    # R 図
    ax2.plot(chart.index, chart["R"], marker="o", color="darkorange")
    ax2.axhline(r_mean, color="green", linestyle="-", label=f"R平均={r_mean:.2f}")
    ax2.axhline(ucl_r, color="red", linestyle="--", label=f"UCL={ucl_r:.2f}")
    ax2.axhline(lcl_r, color="red", linestyle="--", label=f"LCL={lcl_r:.2f}")
    ax2.set_ylabel("範囲 R")
    ax2.set_xlabel("ロット")
    ax2.legend(loc="upper right", fontsize=9)

    plt.setp(ax2.get_xticklabels(), rotation=60, ha="right", fontsize=8)
    fig.tight_layout()
    fig.savefig(path, dpi=150)
    plt.close(fig)


if __name__ == "__main__":  # 5〜6 章の if __name__ ブロックはこれにまとめて OK(残しても動きます)
    df = pd.read_csv("samples.csv")
    save_histogram(df, "hist.png")
    save_xbar_r(df, "xbar_r.png")

これで「ある時点から X̄ がドリフトしている」「特定ロットだけ R が異常に大きい」といった工程の傾向が一目で分かります。

8. パレート図

不良要因の集計に欠かせないパレート図は、棒グラフと累積比率(折れ線)の組み合わせです。Matplotlib では二軸グラフで表現します。

# pareto.py — 別ファイルとして保存(qc_charts.py には追記しない)
import matplotlib_fontja  # noqa: F401
import matplotlib.pyplot as plt
import pandas as pd

# 仮の不良要因データ
defects = pd.DataFrame({
    "cause": ["寸法外れ", "傷", "汚れ", "色ムラ", "刻印不良", "その他"],
    "count": [120, 80, 60, 30, 20, 10],
})

# QC の作法: 「その他」は件数に関係なく必ず最後に置く
others = defects[defects["cause"] == "その他"]
defects = pd.concat([
    defects[defects["cause"] != "その他"].sort_values("count", ascending=False),
    others,
]).reset_index(drop=True)

defects["cumulative"] = defects["count"].cumsum() / defects["count"].sum() * 100

fig, ax1 = plt.subplots(figsize=(8, 4.5))
ax1.bar(defects["cause"], defects["count"], color="steelblue")
ax1.set_ylabel("件数", color="steelblue")
ax1.tick_params(axis="y", labelcolor="steelblue")

ax2 = ax1.twinx()
ax2.plot(defects["cause"], defects["cumulative"], color="darkorange",
         marker="o", linewidth=2)
ax2.set_ylabel("累積比率 (%)", color="darkorange")
ax2.set_ylim(0, 110)
ax2.tick_params(axis="y", labelcolor="darkorange")
ax2.axhline(80, color="gray", linestyle=":")  # 80% ライン

ax1.set_title("不良要因のパレート図")
fig.tight_layout()
fig.savefig("pareto.png", dpi=150)
plt.close(fig)

「上位 2 要因で全体の 70% を占めている」と一目で見せられると、改善の優先順位が定まります。

9. 月次レポートの自動化

ここまでのコードを 1 つにまとめれば、「月初に CSV を入れ替えるだけで、グラフ画像と数表を自動生成する」スクリプトが完成します。5〜7 章で qc_charts.py に書きためた関数(save_histogram / cp_cpk / save_xbar_r)を、ここで import して使います(report.py は qc_charts.py と同じフォルダに置いてください)。

# report.py — qc_charts.py と同じフォルダに置く
from datetime import datetime
from pathlib import Path

import pandas as pd

from qc_charts import cp_cpk, save_histogram, save_xbar_r

# タスクスケジューラ起動でも迷子にならないよう、スクリプト自身の場所を基準にする
BASE = Path(__file__).resolve().parent
OUT_DIR = BASE / "reports" / datetime.now().strftime("%Y-%m")  # 例: reports/2026-07
OUT_DIR.mkdir(parents=True, exist_ok=True)


def main() -> None:
    df = pd.read_csv(BASE / "samples.csv")  # 実運用では実データの CSV パスに差し替え
    USL, LSL = 102.0, 98.0

    # 1) 集計表
    summary = df.groupby("machine")["value"].agg(["mean", "std", "count"])
    summary.to_csv(OUT_DIR / "summary.csv", encoding="utf-8-sig")

    # 2) Cp/Cpk 表
    rows = []
    for m, group in df.groupby("machine"):
        rows.append({"machine": m, **cp_cpk(group["value"], USL, LSL)})
    pd.DataFrame(rows).to_csv(OUT_DIR / "cpk.csv", index=False, encoding="utf-8-sig")

    # 3) ヒストグラム / 管理図 を保存(規格値もここから渡す)
    save_histogram(df, OUT_DIR / "hist.png", usl=USL, lsl=LSL)
    save_xbar_r(df, OUT_DIR / "xbar_r.png")
    # パレート図は別 CSV から作る想定なので省略

    print(f"レポートを {OUT_DIR} に出力しました")


if __name__ == "__main__":
    main()

出力先フォルダは datetime.now() から毎月自動で切り替わります(例: reports/2026-07)。パスを Path(__file__) 基準にしているのは、タスクスケジューラの既定の作業フォルダが C:\Windows\System32 のため、相対パスのままだと「手動では動くのに自動実行だけ失敗する」定番の罠を踏むからです。タスクスケジューラ登録時は「プログラム」に Python のフルパス(venv なら .venv\Scripts\python.exe)、「引数」に report.py のフルパスを指定してください。これで「毎月 1 日 9:00 に実行」と登録すれば、月次レポートが自動で生成されるようになります。最後に Outlook や社内チャットに添付して送るところまで Python で書いてしまえば、作表・作図の人手は不要になります(数値の妥当性判断——外れ値が測定ミスなのか実不良なのか——は引き続き人の仕事です)。

10. Streamlit でブラウザベースのダッシュボードに

静的な月次レポートではなく、対話的に「装置を選ぶ」「期間を絞る」「規格値を変えて Cpk 再計算」ができるダッシュボードが欲しい場合は、Streamlit が最短ルートです。

# dashboard.py — qc_charts.py と同じフォルダに置く
import matplotlib_fontja  # noqa: F401
import matplotlib.pyplot as plt
import pandas as pd
import streamlit as st

from qc_charts import cp_cpk

st.set_page_config(page_title="QC ダッシュボード", layout="wide")
st.title("品質管理ダッシュボード")

@st.cache_data
def load_data() -> pd.DataFrame:
    return pd.read_csv("samples.csv", parse_dates=["measured_at"])


df = load_data()

# サイドバー: 装置選択と規格値
machines = sorted(df["machine"].unique())
selected = st.sidebar.multiselect("装置", machines, default=machines)
usl = st.sidebar.number_input("上限規格", value=102.0)
lsl = st.sidebar.number_input("下限規格", value=98.0)

if not selected:
    st.warning("装置を 1 つ以上選択してください")
    st.stop()

filtered = df[df["machine"].isin(selected)]

col1, col2, col3 = st.columns(3)
col1.metric("件数", f"{len(filtered):,}")
col2.metric("平均", f"{filtered['value'].mean():.3f}")
col3.metric("Cpk", f"{cp_cpk(filtered['value'], usl, lsl)['Cpk']:.3f}")

st.subheader("ヒストグラム")
fig, ax = plt.subplots()
ax.hist(filtered["value"], bins=30, edgecolor="black")
ax.axvline(usl, color="red", linestyle="--")
ax.axvline(lsl, color="red", linestyle="--")
st.pyplot(fig)

st.subheader("装置別サマリ")
st.dataframe(filtered.groupby("machine")["value"].agg(["mean", "std", "count"]))
streamlit run dashboard.py

これだけで、ブラウザに「装置を選ぶ」「規格値を入力する」「即座に Cpk が再計算される」ダッシュボードが立ち上がります(このコマンドで開くのは自分の PC のブラウザです)。Excel ファイルをメールでやり取りする運用から、抜け出す第一歩になります。

「社内 LAN の他の PC からも見られるようにしたい」となったら、公開範囲・認証・PC の常駐化といった設計が必要になります。Streamlit は素の状態では認証なしなので、品質データを載せる場合は公開範囲に注意してください。社内公開・認証・運用までの本格的な構築手順は Streamlit ダッシュボード構築の記事 で扱います。

11. 移行のときに気をつけたいこと

長年使われてきた Excel 集計を Python に置き換えるときの実務的な注意点を最後に整理します。

  • 計算式の移植は必ず「結果突合」する: 同じデータを Excel と Python で計算し、すべての値が一致することを確認してから運用切替する
  • Excel の「見た目」も再現するか決める: 罫線、色、フォントの再現は openpyxl や xlsxwriter で可能だが、コストはそれなりに高い
  • 既存ユーザーには 2〜3 ヶ月の並行運用を提供する: いきなり Python だけにすると、現場の信頼を失う
  • 「誰が更新できるか」の引き継ぎを設計する: 内製化の最大のリスクは「作った人が異動して誰も触れなくなる」こと。コード、リポジトリ(コードの保管場所。Git で変更履歴ごと管理する)、起動手順をドキュメント化する

12. おわりに

pandas と Matplotlib に Streamlit を組み合わせれば、製造業の品質管理に必要なグラフ群はすべて Python で完結できます。Excel で「毎月の作業」になっていたものは、ほとんどが Python では「毎月の自動実行」になります。

本記事のサンプルコードは、すべてダミーデータで再現できる構成になっています。手元の現場で再現可能な範囲から、まずは小さく置き換えてみるのがおすすめです。

関連記事