100本ノック / Python / データ分析のためのPython入門100本ノック

コメントを書く

NumPy で製造ラインの品質データを高速に一括処理する

100本ノック 第7章(No.061〜No.070):NumPy 入門

本記事は「データ分析のための Python 入門 100本ノック」シリーズの 第7章 です。
第6章(No.051〜060)ではファイル操作と CSV を学びました。
本章では NumPy を使って、精密機械部品工場の品質データ(90日間・3ライン)を
Python リストよりも高速かつ簡潔に処理する方法を習得します。

[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。

はじめに:この記事で扱う製造業の実務課題

精密機械部品工場のデータ分析担当 K さんが直面している問題です。

現状
  1. 3ラインの日次生産数・不良数が 90日分 × 3シート に蓄積されている
  2. 不良率の計算に for ループを書いているため、コードが長く保守も大変
  3. 「先月の不良率が最も高かった日はいつか」という質問に答えるたびに
     270個の数値を手作業で探している
  4. ラインごとの安定性(ばらつき)を評価するための統計量を Excel で個別に計算

NumPy を使うと、270個(3ライン × 90日)の不良率を 1行 で計算し、
最大値・最小値・平均・標準偏差を 関数1つ で求め、
警告日(不良率 > 閾値)を ファンシーインデックス で瞬時に抽出できます。

現場でよくある状況

場面課題
不良率の一括計算for ループで90行を逐次処理。コードが長く、行数が増えると遅い
月別・ライン別の平均Excel の AVERAGE 関数を各列・各シートで個別に適用
異常値(警告日)の抽出「不良率 > 2% の日」を手でフィルタリング、見落としのリスクがある
安定性評価各ラインの標準偏差を個別に計算し、変動係数(CV)は手計算
複数ラインの横比較ラインごとにファイルが分かれており全体像を把握しにくい

NumPy の ベクトル演算ブールインデックス集約関数(axis 指定)
組み合わせれば、これらの作業を数行のコードで自動化できます。

なぜこの問題は判断が難しいのか

NumPy は学習コストが低く見えますが、製造データを扱う上では次の落とし穴があります。

  1. axis の方向の混乱
    2次元配列で np.mean(data, axis=0) は「列方向(日別)」の平均、
    axis=1 は「行方向(ライン別)」の平均です。
    shape を先に確認し、「どちらが行でどちらが列か」を把握してから操作しましょう

  2. スライスは view(参照)を返す
    sub = array[10:20] のスライスは コピーではなく参照 です。
    sub[0] = 99 とすると元の array も変わります。
    意図しない変更を防ぐには .copy() を明示します

  3. dtype の落とし穴
    int32 配列同士の除算は float になりますが、
    整数演算(//%)では切り捨てが起きることを意識してください

  4. ブールインデックスはコピーを返す
    array[array > 2.0] = 0 のインプレース代入は元配列を変更しますが、
    sub = array[array > 2.0] は新しいコピーを返します。
    代入と参照では挙動が異なるので注意が必要です

今回扱うノックの全体像

No.タイトル製造業での活用場面
061NumPyの役割を理解するPython リスト vs NumPy の速度・簡潔さを比較
062NumPyをimportするインポートとよく使う関数の確認
063NumPy配列を作成する日次生産数・不良数・寸法測定値の配列化
064配列の形を確認するshape / ndim / dtype / size で2次元データの構造把握
065配列の要素を取り出すインデックス・スライス・2次元アクセスで特定日・特定ラインを抽出
066配列に対して一括計算する不良率・売上・損失を270件同時計算(ブロードキャスト)
067平均値を計算するaxis 指定でライン別・月別平均不良率を一括算出・棒グラフ可視化
068最大値・最小値を計算するargmax / argmin で最悪日・最良日を特定
069標準偏差を計算する変動係数(CV)でラインの品質安定性を横比較
070条件を満たすデータを抽出するnp.where で警告日を自動抽出・散布図で可視化

Python 環境の準備

import subprocess, sys
res = subprocess.run(["sw_vers", "-productVersion"], capture_output=True, text=True)
print(f"macOS : {res.stdout.strip()}")
print(f"Python: {sys.version}")
macOS : 26.3
Python: 3.13.1 (main, Dec  3 2024, 17:59:52) [Clang 16.0.0 (clang-1600.0.26.4)]
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import datetime

matplotlib.rcParams['font.family'] = 'Hiragino Maru Gothic Pro'
%config InlineBackend.figure_format = 'svg'
np.random.seed(42)

print("ライブラリ読み込み完了")
print(f"  NumPy     : {np.__version__}")
print(f"  Matplotlib: {matplotlib.__version__}")
ライブラリ読み込み完了
  NumPy     : 2.5.1
  Matplotlib: 3.11.0

架空データの作成

想定シナリオ: 精密機械部品工場 / 機械加工ライン 品質管理チーム
期間: 2024年1月1日〜3月30日(90日間)
管理対象: 3製品ライン(精密軸・ベアリング・フランジ)
データ形式: 2次元 NumPy 配列 — shape: (3, 90) = (ライン数, 日数)

np.random.seed(42)

# ── 基本設定 ─────────────────────────────────────────────
lines        = ["M1-精密軸", "M2-ベアリング", "M3-フランジ"]
unit_prices  = np.array([2400, 1800, 1200])   # 円/個
n_days       = 90                              # 2024-01-01〜03-30
base_date    = datetime.date(2024, 1, 1)

# 月インデックス(0=1月: 31日, 1=2月: 29日, 2=3月: 30日)
month_idx = np.array([0]*31 + [1]*29 + [2]*30, dtype=np.int32)

# ── 日次生産数(3ライン × 90日)─────────────────────────
base_prod = [450, 380, 550]
prod_2d = np.array([
    np.random.randint(b - 20, b + 21, size=n_days)
    for b in base_prod
], dtype=np.int32)   # shape: (3, 90)

# ── 日次不良数 ───────────────────────────────────────────
dr_base = [0.018, 0.022, 0.014]
defect_2d = np.array([
    np.maximum(1, np.round(
        prod_2d[i] * dr_base[i] + np.random.randn(n_days) * 1.2
    ).astype(int))
    for i in range(3)
], dtype=np.int32)   # shape: (3, 90)

# ── 日次不良率 ───────────────────────────────────────────
defect_rates_2d = defect_2d / prod_2d * 100  # shape: (3, 90), dtype: float64

# ── 確認表示 ─────────────────────────────────────────────
print(f"配列サイズ: prod_2d{prod_2d.shape}, defect_2d{defect_2d.shape}, "
      f"defect_rates_2d{defect_rates_2d.shape}")
print()
print(f"{'ライン':<14} {'週計(生産)':>10} {'週計(不良)':>10} {'平均不良率':>10} {'最大不良率':>10}")
print("=" * 58)
for i, line in enumerate(lines):
    print(f"{line:<14} {int(prod_2d[i].sum()):>10,} {int(defect_2d[i].sum()):>10} "
          f"{float(defect_rates_2d[i].mean()):>9.2f}% {float(defect_rates_2d[i].max()):>9.2f}%")
配列サイズ: prod_2d(3, 90), defect_2d(3, 90), defect_rates_2d(3, 90)

ライン                週計(生産)     週計(不良)      平均不良率      最大不良率
==========================================================
M1-精密軸             40,387        745      1.84%      2.55%
M2-ベアリング           34,339        771      2.24%      2.89%
M3-フランジ            49,581        711      1.43%      1.94%

No.061:NumPyの役割を理解する

実務での意味

製造ラインの日次不良率を計算するとき、Python の for ループでは
90回のループが必要です。NumPy の ベクトル演算 を使えば、
同じ計算が 1行・配列全体に一括適用 で完了します。
データが1万行・10ラインに増えても、NumPy のコードは変わりません。

分析・モデル化の考え方

NumPy は内部で C 言語で実装された配列演算を行うため、
Python の for ループより 10〜100倍高速 です。
数値計算ライブラリ(pandas・scikit-learn・scipy)は内部で NumPy を使っており、
NumPy の配列操作を理解することが高度なデータ分析の基礎になります。

Python で確認する

# Python リスト(for ループ)と NumPy の簡潔さを比較する

production_list = list(prod_2d[0])     # M1 の90日分
defect_list     = list(defect_2d[0])

# ── Pure Python: for ループで不良率を計算 ────────────────
defect_rates_py = []
for prod, def_count in zip(production_list, defect_list):
    defect_rates_py.append(def_count / prod * 100)
mean_py = sum(defect_rates_py) / len(defect_rates_py)

# ── NumPy: ベクトル演算で1行 ─────────────────────────────
defect_rates_np = defect_2d[0] / prod_2d[0] * 100
mean_np = defect_rates_np.mean()

print("=== 計算結果の比較(M1-精密軸, 90日間)===")
print(f"  Python リスト先頭5件: {[round(r, 2) for r in defect_rates_py[:5]]}")
print(f"  NumPy 配列  先頭5件: {defect_rates_np[:5].round(2)}")
print()
print(f"  Python 平均: {mean_py:.4f}%")
print(f"  NumPy 平均 : {mean_np:.4f}%   (誤差なし)")
print()
print("=== コードの違い ===")
print("  Python: for ループ 4行 + sum/len")
print("  NumPy : defect_2d[0] / prod_2d[0] * 100  ← 1行で完了")
print()
print(f"  型の違い: Python={type(defect_rates_py).__name__}, NumPy={type(defect_rates_np).__name__}")
print(f"  NumPy dtype: {defect_rates_np.dtype}")
=== 計算結果の比較(M1-精密軸, 90日間)===
  Python リスト先頭5件: [np.float64(1.71), np.float64(1.53), np.float64(2.03), np.float64(2.06), np.float64(2.0)]
  NumPy 配列  先頭5件: [1.71 1.53 2.03 2.06 2.  ]

  Python 平均: 1.8439%
  NumPy 平均 : 1.8439%   (誤差なし)

=== コードの違い ===
  Python: for ループ 4行 + sum/len
  NumPy : defect_2d[0] / prod_2d[0] * 100  ← 1行で完了

  型の違い: Python=list, NumPy=ndarray
  NumPy dtype: float64

結果の読み取り

Python リストと NumPy 配列で計算結果は完全に一致しています。
defect_2d[0] / prod_2d[0] * 100 の1行で 90件の不良率が計算されました。
これが NumPy のベクトル演算です。
データ件数が 90件 → 9,000件 → 90万件に増えても、NumPy のコードは変わりません。
製造データが増えても保守性の高いコードが書けます。


No.062:NumPyをimportする

実務での意味

import numpy as np が NumPy の標準的なインポート方法です。
np という別名(エイリアス)は業界標準であり、他のライブラリも
NumPy 配列を入力として想定して設計されています。

分析・モデル化の考え方

NumPy はデータ分析エコシステムの土台です。
pandas の Series / DataFrame は NumPy 配列の上に構築されており、
scikit-learn のモデルへの入力も NumPy 配列を使います。
NumPy の基本操作を習得することが、より高度なライブラリへの移行を容易にします。

Python で確認する

import numpy as np

print(f"NumPy バージョン: {np.__version__}")
print()

# 製造データ分析でよく使う NumPy 関数
kpi_funcs = [
    ("np.array()",        "リスト→配列変換"),
    ("np.zeros(n)",       "ゼロ配列の初期化(アラートフラグなど)"),
    ("np.arange(n)",      "連番配列(日次インデックスの生成)"),
    ("np.mean()",         "平均値(不良率の月別平均)"),
    ("np.std()",          "標準偏差(品質安定性評価)"),
    ("np.max() / min()",  "最大・最小(最悪日・最良日)"),
    ("np.argmax()",       "最大値のインデックス(何日目か)"),
    ("np.where()",        "条件による抽出・ラベル付け"),
    ("np.sum(axis=)",     "合計(軸指定で行・列ごとに集計)"),
    ("np.cumsum()",       "累積和(月次累積生産数)"),
]
print(f"{'関数':<28} {'製造データでの活用'}")
print("=" * 68)
for fname, desc in kpi_funcs:
    print(f"  {fname:<26} {desc}")

print()
# NumPy 定数
print(f"np.pi  = {np.pi:.6f}   (円形部品の周長・面積計算)")
print(f"np.inf = {np.inf}       (上限なしの初期値として)")
print(f"np.nan = {np.nan}       (欠損値の表現)")
NumPy バージョン: 2.5.1

関数                           製造データでの活用
====================================================================
  np.array()                 リスト→配列変換
  np.zeros(n)                ゼロ配列の初期化(アラートフラグなど)
  np.arange(n)               連番配列(日次インデックスの生成)
  np.mean()                  平均値(不良率の月別平均)
  np.std()                   標準偏差(品質安定性評価)
  np.max() / min()           最大・最小(最悪日・最良日)
  np.argmax()                最大値のインデックス(何日目か)
  np.where()                 条件による抽出・ラベル付け
  np.sum(axis=)              合計(軸指定で行・列ごとに集計)
  np.cumsum()                累積和(月次累積生産数)

np.pi  = 3.141593   (円形部品の周長・面積計算)
np.inf = inf       (上限なしの初期値として)
np.nan = nan       (欠損値の表現)

結果の読み取り

製造データ分析で最もよく使うのは np.meannp.stdnp.argmaxnp.where の4つです。
np.inf は「上限なし」の初期値として最小値を求めるアルゴリズムで使います。
np.nan は欠損値(センサー停止・記録漏れ)を表現する際に使います。
np.nan を含む配列では np.meannan を返すので、
欠損値がある場合は np.nanmeannp.nanstd を使いましょう。


No.063:NumPy配列を作成する

実務での意味

製造データを NumPy 配列として作成する方法を学びます。
CSV や Excel から読み込んだデータも、最終的には NumPy 配列に変換して処理します。
np.random.normal() を使えば、寸法測定値のシミュレーションデータ
現実に近い形で生成できます。

分析・モデル化の考え方

NumPy 配列の作成には大きく4つの方法があります。
np.array() でリストから変換、②np.zeros/ones() で初期化、
np.arange/linspace() で等間隔数列、④np.random.*() でランダムデータです。
製造現場の寸法測定値は正規分布 N(μ,σ2)N(\mu, \sigma^2) に従うことが多く、
np.random.normal(mean, std, size) で忠実にシミュレーションできます。

Python で確認する

# NumPy 配列の4つの作成方法

# 1. np.array() でリストから変換
weekly_prod_063 = np.array([450, 462, 438, 471, 445, 457, 448])
print(f"1. np.array() : {weekly_prod_063}  shape={weekly_prod_063.shape}")

# 2. np.zeros() で初期化(アラートフラグなど)
alert_flags = np.zeros(n_days, dtype=np.int8)
print(f"2. np.zeros() : {alert_flags[:5]} ...  shape={alert_flags.shape}, dtype={alert_flags.dtype}")

# 3. np.arange() で連番
day_indices = np.arange(n_days)           # 0〜89
print(f"3. np.arange(): {day_indices[:7]} ...  shape={day_indices.shape}")

# 4. np.random.normal() で寸法測定値を生成(精密軸の軸径検査)
target_diam  = 25.000   # 目標直径 mm
tolerance    = 0.050    # 公差 ±0.05mm
sigma_diam   = tolerance / 3   # 3σ = 公差全幅 → σ = 0.0167mm
measurements = np.random.normal(target_diam, sigma_diam, size=30)

print()
print("=== M1-精密軸 軸径測定値(n=30 サンプル) ===")
print(f"  目標値   : {target_diam:.3f} mm  公差: ±{tolerance:.3f} mm")
print(f"  先頭5件  : {measurements[:5].round(4)} mm")
print(f"  平均     : {measurements.mean():.4f} mm")
print(f"  範囲     : {measurements.min():.4f}{measurements.max():.4f} mm")
print()

# dtype の指定
arr_int   = np.array([1.7, 2.3, 3.9], dtype=np.int32)   # 小数を切り捨て
arr_float = np.array([1, 2, 3],        dtype=np.float64)
print(f"dtype=int32  変換: {arr_int}   (小数は切り捨て)")
print(f"dtype=float64変換: {arr_float}")
1. np.array() : [450 462 438 471 445 457 448]  shape=(7,)
2. np.zeros() : [0 0 0 0 0] ...  shape=(90,), dtype=int8
3. np.arange(): [0 1 2 3 4 5 6] ...  shape=(90,)

=== M1-精密軸 軸径測定値(n=30 サンプル) ===
  目標値   : 25.000 mm  公差: ±0.050 mm
  先頭5件  : [25.0187 24.976  24.998  24.9987 24.9816] mm
  平均     : 24.9983 mm
  範囲     : 24.9620 〜 25.0396 mm

dtype=int32  変換: [1 2 3]   (小数は切り捨て)
dtype=float64変換: [1. 2. 3.]

結果の読み取り

np.random.normal(25.000, 0.0167, size=30) で軸径の正規分布測定値をシミュレートしました。
公差の設計として「3σ = 公差全幅(0.1mm)」を使っているため、
品質工学の「±3σ 管理」に対応しています。
理論上 99.73% の製品が公差内に入ります。
dtype=np.int32float を変換すると小数が切り捨てられます。
int() 関数と挙動は同じですが、配列全体に一括適用できる点が NumPy の強みです。


No.064:配列の形を確認する

実務での意味

2次元配列(行列)として製造データを扱う場合、
「行がラインで列が日付」なのか「行が日付で列がラインなのか」を
必ず shape で確認してから操作することが重要です。
軸の方向を間違えると平均・集計の結果が完全に逆転します。

分析・モデル化の考え方

shapendimdtypesize の4属性は NumPy 配列の「住所」です。
配列操作の前に print(array.shape) を1行追加する習慣を持てば、
axis の方向ミスによるバグの大半は防げます。

Python で確認する

# shape / ndim / dtype / size で配列の構造を把握する

print("=== 1次元配列(M1 日次生産数)===")
a1 = prod_2d[0]                           # shape: (90,)
print(f"  shape: {a1.shape}{a1.shape[0]} 日分")
print(f"  ndim : {a1.ndim}     → 1次元")
print(f"  dtype: {a1.dtype} → 整数型")
print(f"  size : {a1.size}    → 要素数")
print()

print("=== 2次元配列(全ライン × 全日)===")
print(f"  shape: {prod_2d.shape}")
print(f"    → 行 = {prod_2d.shape[0]}ライン(M1, M2, M3)")
print(f"    → 列 = {prod_2d.shape[1]}日(2024-01-01〜03-30)")
print(f"  ndim : {prod_2d.ndim}     → 2次元")
print(f"  dtype: {prod_2d.dtype}")
print(f"  size : {prod_2d.size}    → 全要素数({prod_2d.shape[0]} × {prod_2d.shape[1]})")
print()

print("=== 不良率配列(float64)===")
print(f"  shape: {defect_rates_2d.shape}, dtype: {defect_rates_2d.dtype}")
print()

# shape を使った axis の確認
print("=== axis=0 vs axis=1 の違い(np.sum で確認)===")
sum_axis0 = prod_2d.sum(axis=0)   # 各日の全ライン合計 → shape: (90,)
sum_axis1 = prod_2d.sum(axis=1)   # 各ラインの全日合計 → shape: (3,)
print(f"  axis=0(列方向、日ごと合計): shape={sum_axis0.shape}  先頭3日={sum_axis0[:3]}")
print(f"  axis=1(行方向、ライン合計): shape={sum_axis1.shape}  値={sum_axis1}")
print()
for i, line in enumerate(lines):
    print(f"  {line} 90日間合計生産: {int(sum_axis1[i]):,} 個")
=== 1次元配列(M1 日次生産数)===
  shape: (90,)  → 90 日分
  ndim : 1     → 1次元
  dtype: int32 → 整数型
  size : 90    → 要素数

=== 2次元配列(全ライン × 全日)===
  shape: (3, 90)
    → 行 = 3ライン(M1, M2, M3)
    → 列 = 90日(2024-01-01〜03-30)
  ndim : 2     → 2次元
  dtype: int32
  size : 270    → 全要素数(3 × 90)

=== 不良率配列(float64)===
  shape: (3, 90), dtype: float64

=== axis=0 vs axis=1 の違い(np.sum で確認)===
  axis=0(列方向、日ごと合計): shape=(90,)  先頭3日=[1414 1416 1369]
  axis=1(行方向、ライン合計): shape=(3,)  値=[40387 34339 49581]

  M1-精密軸 90日間合計生産: 40,387 個
  M2-ベアリング 90日間合計生産: 34,339 個
  M3-フランジ 90日間合計生産: 49,581 個

結果の読み取り

prod_2d.shape = (3, 90) から「行 = 3ライン、列 = 90日」が確認できます。
axis=0 は行方向(列をまたいで集計)なので「各日の全ライン合計」、
axis=1 は列方向(行をまたいで集計)なので「各ラインの全日合計」になります。
np.mean(prod_2d, axis=1) で「ライン別の日次平均生産数」が計算できます。
操作前に必ず shape を確認し、axis の方向を意識することが重要です。


No.065:配列の要素を取り出す

実務での意味

「M2ラインの第2週のデータだけ取り出す」「全ラインの1月1日のデータを比較する」
というような 特定日・特定ラインへのアクセス は製造データ分析で日常的に必要です。
NumPy のインデックスとスライスを使えば、これを簡潔に記述できます。

分析・モデル化の考え方

NumPy のスライスは view(参照)を返します。コピーが必要な場合は .copy() を使います。
ファンシーインデックス(整数配列・ブール配列によるインデックス)はコピーを返します。
データ加工の際にどちらを使っているかを意識することが、バグを防ぐ鍵です。

Python で確認する

# インデックス・スライス・2次元アクセスで要素を取り出す

prod_m1 = prod_2d[0]   # M1ライン, shape: (90,)

print("=== 1次元インデックスアクセス ===")
print(f"  初日 [0]    : {prod_m1[0]:,} 個")
print(f"  末日 [-1]   : {prod_m1[-1]:,} 個")
print(f"  10日目 [9]  : {prod_m1[9]:,} 個")
print()

print("=== スライスで週単位に取り出す ===")
week1 = prod_m1[:7]    # 第1週(1/1〜1/7)
week2 = prod_m1[7:14]  # 第2週(1/8〜1/14)
print(f"  第1週 ( 1/ 1〜 1/ 7): {week1}  合計={int(week1.sum()):,}個")
print(f"  第2週 ( 1/ 8〜 1/14): {week2}  合計={int(week2.sum()):,}個")
print()

print("=== 2次元配列のアクセス ===")
print(f"  M1 の1日目   prod_2d[0, 0]   = {prod_2d[0, 0]:,} 個")
print(f"  M2 の1日目   prod_2d[1, 0]   = {prod_2d[1, 0]:,} 個")
print(f"  全ラインの1日目 prod_2d[:, 0] = {prod_2d[:, 0]}  (各ライン生産数)")
print(f"  M3 の全90日  prod_2d[2, :].shape = {prod_2d[2, :].shape}")
print()

print("=== スライスは view → .copy() でコピー ===")
sub = prod_m1[10:15].copy()   # コピーを取得
original_val = prod_m1[10]
sub[0] = 9999
print(f"  sub[0] を 9999 に変更 → 元配列 prod_m1[10] = {prod_m1[10]}  (変化なし)")
=== 1次元インデックスアクセス ===
  初日 [0]    : 468 個
  末日 [-1]   : 464 個
  10日目 [9]  : 440 個

=== スライスで週単位に取り出す ===
  第1週 ( 1/ 1〜 1/ 7): [468 458 444 437 450 468 448]  合計=3,173個
  第2週 ( 1/ 8〜 1/14): [452 440 440 453 465 469 453]  合計=3,172個

=== 2次元配列のアクセス ===
  M1 の1日目   prod_2d[0, 0]   = 468 個
  M2 の1日目   prod_2d[1, 0]   = 394 個
  全ラインの1日目 prod_2d[:, 0] = [468 394 552]  (各ライン生産数)
  M3 の全90日  prod_2d[2, :].shape = (90,)

=== スライスは view → .copy() でコピー ===
  sub[0] を 9999 に変更 → 元配列 prod_m1[10] = 453  (変化なし)

結果の読み取り

prod_2d[:, 0] は「全ライン(すべての行)の1日目(列0)」というスライスで、
3ライン全員の当日生産数をまとめて取り出しています。
.copy() でコピーを作成することで、元の配列への影響を防止できました。
sub[0] = 9999 とした後も prod_m1[10] の値が変わっていないことで
コピーが正しく機能していることが確認できます。


No.066:配列に対して一括計算する

実務での意味

NumPy のブロードキャスト機能により、形の異なる配列同士も
自動的に形を合わせて演算できます。
unit_prices(形 (3,))を (3, 1) に変形すれば、
(3, 90) の生産数配列と掛け算して、
全ライン・全日分の売上を ループなしで一括計算 できます。

分析・モデル化の考え方

ブロードキャストのルール: 形が異なる2つの配列を演算するとき、
各次元が「同じか1かどちらか」であれば、形が1の次元が自動的に拡張されます。
(3,1) × (3,90)(3,90) という変換が自動で行われます。

Python で確認する

# ベクトル演算とブロードキャストで 270件の KPI を一括計算する

# 良品数(ベクトル減算)
good_units_2d = prod_2d - defect_2d          # shape: (3, 90)

# 売上(ブロードキャスト: unit_prices (3,) → (3,1) に変形)
revenue_2d = good_units_2d * unit_prices.reshape(3, 1)  # shape: (3, 90)

# 損失(不良数 × 単価 × (1 + 廃棄コスト率))
penalty = 0.30
loss_2d = defect_2d * unit_prices.reshape(3, 1) * (1 + penalty)   # shape: (3, 90)

print("=== ベクトル演算の結果(各ライン先頭3日)===")
print(f"{'ライン':<14} {'日':>4} {'生産':>6} {'不良':>5} {'不良率(%)':>10} {'良品売上':>12} {'不良損失':>10}")
print("-" * 66)
for i, line in enumerate(lines):
    for j in range(3):
        dr = float(defect_rates_2d[i, j])
        print(f"{line if j==0 else '':<14} {j+1:>4} {int(prod_2d[i,j]):>6,} "
              f"{int(defect_2d[i,j]):>5} {dr:>9.2f}% "
              f"{int(revenue_2d[i,j]):>12,} {int(loss_2d[i,j]):>10,}")

print()
print("=== 週次集計(スライス + sum)===")
for i, line in enumerate(lines):
    w1_prod    = int(prod_2d[i, :7].sum())
    w1_revenue = int(revenue_2d[i, :7].sum())
    w1_loss    = int(loss_2d[i, :7].sum())
    print(f"  {line}: 第1週生産 {w1_prod:,}個 / 売上 {w1_revenue:,}円 / 損失 {w1_loss:,}円")

print()
print("=== 全期間・全ライン合計 ===")
print(f"  総生産数 : {int(prod_2d.sum()):,} 個")
print(f"  総売上   : {int(revenue_2d.sum()):,} 円")
print(f"  総損失   : {int(loss_2d.sum()):,} 円")
=== ベクトル演算の結果(各ライン先頭3日)===
ライン               日     生産    不良     不良率(%)         良品売上       不良損失
------------------------------------------------------------------
M1-精密軸            1    468     8      1.71%    1,104,000     24,960
                  2    458     7      1.53%    1,082,400     21,840
                  3    444     9      2.03%    1,044,000     28,080
M2-ベアリング          1    394     7      1.78%      696,600     16,380
                  2    392    11      2.81%      685,800     25,740
                  3    364     7      1.92%      642,600     16,380
M3-フランジ           1    552     7      1.27%      654,000     10,920
                  2    566     8      1.41%      669,600     12,480
                  3    561     9      1.60%      662,400     14,040

=== 週次集計(スライス + sum)===
  M1-精密軸: 第1週生産 3,173個 / 売上 7,473,600円 / 損失 184,080円
  M2-ベアリング: 第1週生産 2,701個 / 売上 4,753,800円 / 損失 140,400円
  M3-フランジ: 第1週生産 3,850個 / 売上 4,554,000円 / 損失 85,800円

=== 全期間・全ライン合計 ===
  総生産数 : 124,307 個
  総売上   : 214,207,200 円
  総損失   : 5,237,700 円

結果の読み取り

unit_prices.reshape(3, 1) によってブロードキャストが機能し、
(3, 90) の配列全体に各ラインの単価が正しく掛けられました。
ループなしで270件(3ライン × 90日)の売上・損失が瞬時に計算されています。
prod_2d.sum() は配列全体の合計(全ライン・全日の総生産数)を返します。
prod_2d.sum(axis=1) にすればライン別合計、axis=0 なら日別合計になります。


No.067:平均値を計算する

実務での意味

np.mean()axis を指定することで、
「ライン別の平均不良率」と「日別の全ライン平均不良率」を
同じ関数で使い分けることができます。
月別の集計も month_idx とブールマスクを組み合わせれば
ループなしで実現できます。

分析・モデル化の考え方

axis=0 は「行を潰して列方向に集計」→ 日別の集計(90個の結果)
axis=1 は「列を潰して行方向に集計」→ ライン別の集計(3個の結果)
加重平均が必要な場合は np.average(data, weights=weights) を使います。

Python で確認する

# np.mean() で軸を指定した集計

# ライン別の平均不良率(各行の平均)
line_mean_dr = np.mean(defect_rates_2d, axis=1)   # shape: (3,)

# 日別の全ライン平均不良率(各列の平均)
day_mean_dr  = np.mean(defect_rates_2d, axis=0)   # shape: (90,)

print("=== ライン別 平均不良率(axis=1)===")
for i, line in enumerate(lines):
    print(f"  {line}: {line_mean_dr[i]:.3f}%")
print()

# 月別平均(month_idx を使ったブールマスク集計)
month_names_j = ["1月", "2月", "3月"]
print(f"=== ライン別・月別平均不良率 ===")
print(f"{'ライン':<14} {'1月':>8} {'2月':>8} {'3月':>8}")
print("-" * 42)
monthly_avg_067 = np.array([
    [np.mean(defect_rates_2d[i][month_idx == m]) for m in range(3)]
    for i in range(3)
])
for i, line in enumerate(lines):
    row = "  ".join(f"{monthly_avg_067[i, m]:.2f}%" for m in range(3))
    print(f"  {line:<14} {row}")

print()
print(f"  日別平均(先頭5日): {day_mean_dr[:5].round(3)}")
=== ライン別 平均不良率(axis=1)===
  M1-精密軸: 1.844%
  M2-ベアリング: 2.244%
  M3-フランジ: 1.434%

=== ライン別・月別平均不良率 ===
ライン                  1月       2月       3月
------------------------------------------
  M1-精密軸         1.83%  1.86%  1.84%
  M2-ベアリング       2.23%  2.26%  2.24%
  M3-フランジ        1.43%  1.41%  1.46%

  日別平均(先頭5日): [1.585 1.916 1.851 1.807 1.92 ]
# 月別平均不良率の比較棒グラフ
fig, ax = plt.subplots(figsize=(10, 5))

x = np.arange(3)
width = 0.28
colors_067 = ["#4C72B0", "#DD8452", "#55A868"]

for i, line in enumerate(lines):
    ax.bar(x + (i - 1) * width, monthly_avg_067[i], width,
           label=line, color=colors_067[i], alpha=0.88)

ax.axhline(2.0, color="red", linewidth=1.5, linestyle="--", alpha=0.7,
           label="警告ライン(2.0%)")
ax.set_title("製品ライン別 月次平均不良率(np.mean + axis 集計)",
             fontsize=13, pad=10)
ax.set_xlabel("月", fontsize=11)
ax.set_ylabel("平均不良率(%)", fontsize=11)
ax.set_xticks(x)
ax.set_xticklabels(month_names_j)
ax.legend(fontsize=10)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()

svg

結果の読み取り

axis=1(3, 90) 配列の各行(ライン)の平均を計算し、
3つのライン別平均不良率を1行で取得できました。
棒グラフから月ごとの不良率変化をライン横断で比較できます。
M2-ベアリングは全月で警告ライン(2.0%)に近い水準で推移しており、
改善優先ラインとして特定できます。
月次トレンドが改善傾向か悪化傾向かも視覚的に確認できます。


No.068:最大値・最小値を計算する

実務での意味

「不良率が最も高かった日はいつか」「生産数が最も少なかった日はどのラインか」
という質問に答えるには、最大・最小値とそのインデックス(何日目か)の両方が必要です。
np.argmax() / np.argmin() を使えば、値と日付を同時に特定できます。

分析・モデル化の考え方

np.max() は最大値を返しますが、「どの位置か」は np.argmax() で取得します。
日次インデックス(0始まり)から実際の日付への変換には
base_date + datetime.timedelta(days=idx) を使います。
月内の「ワースト日」を定期的に特定してレポートする処理の自動化に活用できます。

Python で確認する

import datetime

# 全期間の最大・最小不良率
overall_max = float(np.max(defect_rates_2d))
overall_min = float(np.min(defect_rates_2d))

print(f"=== 全ライン・全期間の不良率極値 ===")
print(f"  最大不良率 : {overall_max:.2f}%")
print(f"  最小不良率 : {overall_min:.2f}%")
print()

# ライン別の最悪日・最良日を特定
print(f"{'ライン':<14} {'最大不良率':>10} {'最悪日':>10} {'最小不良率':>10} {'最良日':>10}")
print("=" * 56)
for i, line in enumerate(lines):
    max_dr   = float(defect_rates_2d[i].max())
    min_dr   = float(defect_rates_2d[i].min())
    max_day  = int(np.argmax(defect_rates_2d[i]))
    min_day  = int(np.argmin(defect_rates_2d[i]))
    max_date = (base_date + datetime.timedelta(days=max_day)).strftime("%m/%d")
    min_date = (base_date + datetime.timedelta(days=min_day)).strftime("%m/%d")
    print(f"  {line:<14} {max_dr:>9.2f}% {max_date:>10} {min_dr:>9.2f}% {min_date:>10}")

print()
# 全ライン合計で最も生産が多かった日
daily_total_prod = prod_2d.sum(axis=0)    # shape: (90,)
best_day_idx     = int(np.argmax(daily_total_prod))
worst_day_idx    = int(np.argmin(daily_total_prod))
best_date  = (base_date + datetime.timedelta(days=best_day_idx)).strftime("%m/%d")
worst_date = (base_date + datetime.timedelta(days=worst_day_idx)).strftime("%m/%d")
print(f"=== 全ライン合計 生産数の極値 ===")
print(f"  最大生産日: {best_date}  ({int(daily_total_prod[best_day_idx]):,} 個)")
print(f"  最小生産日: {worst_date}  ({int(daily_total_prod[worst_day_idx]):,} 個)")
=== 全ライン・全期間の不良率極値 ===
  最大不良率 : 2.89%
  最小不良率 : 0.90%

ライン                 最大不良率        最悪日      最小不良率        最良日
========================================================
  M1-精密軸              2.55%      01/15      1.32%      03/20
  M2-ベアリング            2.89%      01/18      1.53%      01/11
  M3-フランジ             1.94%      01/15      0.90%      03/20

=== 全ライン合計 生産数の極値 ===
  最大生産日: 03/02  (1,433 個)
  最小生産日: 03/24  (1,322 個)

結果の読み取り

np.argmax(defect_rates_2d[i]) で各ラインの「最悪日」のインデックスが取れ、
base_date + timedelta(days=idx) で実際の日付に変換しました。
最悪日(最大不良率日)がラインによって異なる場合、原因分析は個別に実施する必要があります。
同じ日に複数ラインの不良率が高い場合は、共通要因(材料ロット・作業員・環境)の
調査を優先すると効率的です。


No.069:標準偏差を計算する

実務での意味

不良率の平均が同じでも、ばらつき(標準偏差) が大きいラインは
日によって品質が安定しておらず、管理が難しい状態です。
変動係数(CV = 標準偏差 / 平均) を使うと、
単位や水準が異なるライン同士の安定性を横比較できます。

分析・モデル化の考え方

np.std(data)母標準偏差(分母 = n)を返します。
サンプルを用いた推定には np.std(data, ddof=1)(標本標準偏差、分母 = n-1)を使います。
製造現場では「今月の全生産日 = 母集団」として母標準偏差を使うケースが多いです。
寸法測定のような 工程能力指数(Cp / Cpk) の計算では標準偏差が基本指標になります。

Python で確認する

# np.std(), np.var() で品質安定性を評価する

print(f"{'ライン':<14} {'平均不良率':>10} {'標準偏差':>10} {'変動係数CV':>12} {'安定性評価':>12}")
print("=" * 62)
for i, line in enumerate(lines):
    mean_v = float(np.mean(defect_rates_2d[i]))
    std_v  = float(np.std(defect_rates_2d[i]))       # 母標準偏差
    cv_pct = std_v / mean_v * 100
    stability = "◎ 安定" if cv_pct < 10 else ("○ 普通" if cv_pct < 20 else "△ 不安定")
    print(f"  {line:<14} {mean_v:>9.2f}% {std_v:>9.3f}% {cv_pct:>10.1f}% {stability:>12}")

print()

# 寸法検査:工程能力指数(Cp)の計算
print("=== M1-精密軸 軸径の工程能力評価 ===")
mu_m    = float(np.mean(measurements))
sigma_m = float(np.std(measurements))    # 母標準偏差
usl     = target_diam + tolerance        # 上限規格値
lsl     = target_diam - tolerance        # 下限規格値
cp      = (usl - lsl) / (6 * sigma_m)   # 工程能力指数 Cp
cpk     = min(usl - mu_m, mu_m - lsl) / (3 * sigma_m)   # Cpk(偏り考慮)

print(f"  目標値: {target_diam:.3f} mm  公差: ±{tolerance:.3f} mm")
print(f"  測定平均: {mu_m:.4f} mm  標準偏差: {sigma_m:.4f} mm")
print(f"  Cp  = {cp:.2f}  (1.33 以上で合格: {'✅ 合格' if cp >= 1.33 else '❌ 要改善'})")
print(f"  Cpk = {cpk:.2f}  (1.33 以上で合格: {'✅ 合格' if cpk >= 1.33 else '❌ 要改善'})")
print()
n_ng = int(np.sum((measurements < lsl) | (measurements > usl)))
print(f"  公差外品: {n_ng} 個 / {len(measurements)} 個")
ライン                 平均不良率       標準偏差       変動係数CV        安定性評価
==============================================================
  M1-精密軸              1.84%     0.268%       14.6%         ○ 普通
  M2-ベアリング            2.24%     0.319%       14.2%         ○ 普通
  M3-フランジ             1.43%     0.217%       15.1%         ○ 普通

=== M1-精密軸 軸径の工程能力評価 ===
  目標値: 25.000 mm  公差: ±0.050 mm
  測定平均: 24.9983 mm  標準偏差: 0.0165 mm
  Cp  = 1.01  (1.33 以上で合格: ❌ 要改善)
  Cpk = 0.98  (1.33 以上で合格: ❌ 要改善)

  公差外品: 0 個 / 30 個

結果の読み取り

変動係数(CV)を見ると、M3-フランジは CV が高い場合、
平均不良率は低くても日ごとのばらつきが大きい「不安定なライン」であることがわかります。
工程能力指数 Cp は「規格幅 / (6σ)」で計算されます。
Cp ≥ 1.33(工程能力が十分)の場合、理論的に不良品率は 66ppm(0.0066%)以下です。
Cpk は工程の中心ズレも考慮した指数で、Cp > Cpk の場合は
「ばらつきは小さいが平均が目標からずれている」ことを意味します。


No.070:条件を満たすデータを抽出する

実務での意味

「不良率が警告ライン(2.0%)を超えた日を自動抽出して報告する」という処理は、
製造データパイプラインで最も頻繁に必要となる操作の1つです。
NumPy のブールインデックスnp.where() を使えば、
条件フィルタリングをベクトル演算で高速に実行できます。

分析・モデル化の考え方

array[array > 閾値] はブールインデックスによるフィルタリングで、
条件を満たす要素だけを新しい配列として返します(コピー)。
np.where(条件, 真の値, 偽の値) は条件に応じて2つの値を選択する
三項演算子の配列版 です。管理グラフの警告ラベル自動付与などに活用できます。

Python で確認する

# ブールインデックスと np.where() で警告日を抽出する
threshold = 2.0  # 警告ライン(不良率 2.0%)

print("=== ブールインデックスによる警告日抽出 ===")
for i, line in enumerate(lines):
    alert_mask  = defect_rates_2d[i] > threshold    # boolean array, shape: (90,)
    alert_count = int(alert_mask.sum())
    if alert_count > 0:
        alert_dr = defect_rates_2d[i][alert_mask]   # ファンシーインデックス
        print(f"  {line}: 警告日 {alert_count:2d}日 / {n_days}日  "
              f"最大 {float(alert_dr.max()):.2f}%  警告日平均 {float(alert_dr.mean()):.2f}%")
    else:
        print(f"  {line}: 警告日  0日 / {n_days}日  (全日正常)")

print()

# np.where() でラベルを付与(M1 先頭10日)
labels_m1 = np.where(defect_rates_2d[0] > threshold, "🔴 警告", "✅ 正常")
print("=== np.where() による状態ラベル(M1, 先頭10日)===")
for j in range(10):
    d = (base_date + datetime.timedelta(days=j)).strftime("%m/%d")
    print(f"  {d}: {defect_rates_2d[0, j]:.2f}%  {labels_m1[j]}")

print()
# 複数ラインが同時に警告した日
simultaneous = int(np.sum(np.sum(defect_rates_2d > threshold, axis=0) >= 2))
print(f"2ライン以上が同日に警告した日数: {simultaneous} 日")
=== ブールインデックスによる警告日抽出 ===
  M1-精密軸: 警告日 26日 / 90日  最大 2.55%  警告日平均 2.17%
  M2-ベアリング: 警告日 71日 / 90日  最大 2.89%  警告日平均 2.36%
  M3-フランジ: 警告日  0日 / 90日  (全日正常)

=== np.where() による状態ラベル(M1, 先頭10日)===
  01/01: 1.71%  ✅ 正常
  01/02: 1.53%  ✅ 正常
  01/03: 2.03%  🔴 警告
  01/04: 2.06%  🔴 警告
  01/05: 2.00%  ✅ 正常
  01/06: 2.14%  🔴 警告
  01/07: 1.56%  ✅ 正常
  01/08: 1.77%  ✅ 正常
  01/09: 1.36%  ✅ 正常
  01/10: 1.59%  ✅ 正常

2ライン以上が同日に警告した日数: 21 日
# 全ライン日次不良率推移 + 警告日ハイライト(3パネル)
fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)
colors_070 = ["#4C72B0", "#DD8452", "#55A868"]
day_range  = np.arange(n_days)

for i, (ax, line) in enumerate(zip(axes, lines)):
    dr         = defect_rates_2d[i]
    alert_mask = dr > threshold

    ax.plot(day_range, dr, color=colors_070[i], linewidth=1.5, alpha=0.7)
    ax.scatter(day_range[alert_mask], dr[alert_mask],
               color="red", zorder=5, s=45, label=f"警告日({int(alert_mask.sum())}日)")
    ax.axhline(threshold, color="red", linestyle="--", linewidth=1.0, alpha=0.6)
    ax.set_title(f"{line}  ─  警告日: {int(alert_mask.sum())} 日 / {n_days} 日", fontsize=11)
    ax.set_ylabel("不良率(%)", fontsize=10)
    ax.legend(fontsize=9)
    ax.grid(alpha=0.3)

axes[-1].set_xlabel("日次インデックス(0 = 2024-01-01)", fontsize=11)
plt.suptitle("製品ライン別 日次不良率推移と警告日抽出(np.where / ブールインデックス)",
             fontsize=13, y=1.01)
plt.tight_layout()
plt.show()

svg

結果の読み取り

赤点が「不良率 > 2.0%」の警告日です。
defect_rates_2d[i][alert_mask] のブールインデックスで、
条件を満たす日のデータだけを取り出し、最大値・平均値を計算しました。
グラフで視覚的に警告日のパターンを確認できます。
「週末前に不良率が上がる」「特定の時期にクラスター状に発生する」などの
パターンが見えた場合、原因(設備メンテナンス・素材ロット・作業員)を
追跡するための手がかりになります。


対象ノックを通して見える実務上の示唆

No.061〜070 で学んだ NumPy の基礎は、製造データ分析の 高速化と精度向上 の基盤です。

1. ベクトル演算 = 「コードの簡潔化」と「スケーラビリティ」

for ループで書いた不良率計算が1行になるだけでなく、
ライン数が3本→30本に増えても (30, 90) の配列が (3, 90) と同じコードで動きます。
製造データのスケールアップに強いコードが書けます。

2. axis の指定 = 「集計粒度の制御」

axis=0(日別集計)と axis=1(ライン別集計)を使い分けることで、
「どの次元を残すか」を自分でコントロールできます。
「月別・ライン別・シフト別」などの多次元集計は
NumPy の axis と pandas の groupby の組み合わせで実現します。

3. ブールインデックス = 「動的フィルタ」

警告ライン・管理限界・公差の閾値を変数にしておくことで、
defect_rates_2d[defect_rates_2d > threshold] の1行が
「閾値変更による再集計」に即対応できます。
閾値をパラメータ化することが保守性の高い品質管理システムの設計です。

4. 工程能力指数(Cp / Cpk)の自動計算

np.std と公差設計値から Cp / Cpk を自動計算する処理は、
定期品質報告書の自動生成に直結します。
第9章(Polars によるデータ加工)では複数工程のデータを
一括で処理する仕組みを構築します。

実務導入する場合に必要なこと

本章で学んだ NumPy を製造データ分析システムに導入する際の手順です。

Step 1: CSV データの NumPy 配列変換(約半日)

既存の Excel / CSV データを np.loadtxt() または np.genfromtxt() で読み込みます。
欠損値がある場合は np.genfromtxt(file, filling_values=np.nan) を使います。

Step 2: 2次元配列の設計(約1日)

「どの軸がラインでどの軸が日付か」を最初に決め、
(n_lines, n_days) の統一フォーマットで全データを管理します。
軸設計を統一することで axis 指定のミスが防げます。

Step 3: 閾値パラメータのファイル化(約半日)

警告ライン・管理限界・公差などの閾値を CSV やJSON ファイルから読み込む設計にします。
コードを修正せずに閾値変更だけで再計算できるようになります。

Step 4: 工程能力指数の自動レポート化(約1日)

本章 No.069 の Cp / Cpk 計算を全ラインに適用し、
月次品質報告書を自動生成するスクリプトを構築します。
第10章(集計・可視化・ミニ分析)ではこれをより本格的なレポートに発展させます。

まとめ

本章(No.061〜070)で学んだ内容を整理します。

No.スキル製造現場での活用
061NumPyの役割を理解するPython リストとの速度・可読性比較、ベクトル演算の実感
062NumPyをimportする主要関数の一覧と製造データ分析での使い所の把握
063NumPy配列を作成する生産数・不良数・寸法測定値の配列化
064配列の形を確認するshape/ndim/dtype/size で2次元構造の把握
065配列の要素を取り出すインデックス・スライス・view vs copy の理解
066配列に対して一括計算するブロードキャストで270件の KPI を1行計算
067平均値を計算するaxis 指定でライン別・月別平均を一括算出・棒グラフ可視化
068最大値・最小値を計算するargmax/argmin で最悪日・最良日の日付特定
069標準偏差を計算する変動係数で安定性評価・Cp/Cpk で工程能力指数の算出
070条件を満たすデータを抽出するブールインデックスで警告日自動抽出・散布図で可視化

第8章(No.071〜080)では Polars を使って、
本章の NumPy 配列を DataFrame として扱い、
列名付きの集計・フィルタリング・CSV 読み込みを学びます。

法人向けのご相談


数理工房 では、製造業・DX 推進担当者向けの Python 研修・データ分析支援を提供しています。

こんなお悩みはありませんか?

  • 「本章の不良率・工程能力計算を自社の実データで動かしたい」
  • 「NumPy で品質管理システムを自動化したい」
  • 「管理図(Xbar-R 管理図)・工程能力指数の自動算出ツールを作りたい」
  • 「社内の Python 研修をカスタマイズして製造業向けに実施してほしい」

提供サービス

サービス概要
製造業向け Python 研修現場データを使った実践型研修(オンライン/対面)
品質管理自動化システム不良率・Cp/Cpk・管理図の自動算出・レポート化
センサーデータ解析基盤時系列データの異常検知・予兆保全システム構築
DX 推進コンサルティング課題整理から実装・定着支援まで一貫対応

📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。