100本ノック / 機械学習 / 実務機械学習100本ノック

製造業のデータ分析入門|CSV読込から相関分析までPython実践10本ノック

製造データを「判断できる情報」に変える:生産・品質・設備データ分析の最初の10ノック

製造現場では、データを集めること自体よりも、そのデータを信頼して会議や改善活動に使える状態へ整えることが重要です。本記事では、架空の精密部品工場の日次データを題材に、CSVの受入確認から分布・散布図・相関係数までを一つの診断フローとして実践します。

この「機械学習100本ノック」では、データ確認、前処理、回帰・分類、モデル検証、特徴量設計、需要予測、異常検知、推薦・クラスタリング、軽量なMLOpsまでを段階的に扱います。目的は手法の暗記ではなく、現場の問いを分析可能な形にし、意思決定へ接続する力を身につけることです。今回はその入口となる No.001〜No.010 を扱います。

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

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

想定する問いは「直近の不良率悪化に対し、どのライン・製品・設備条件から調査すべきか」です。日報CSVには生産数量、停止時間、温度、振動、不良数が入っています。しかし、列の意味や欠損、分布を確かめずに平均値や機械学習モデルだけを出すと、入力ミスや母集団構成の違いを設備異常と誤認しかねません。

本記事の到達点は、データの品質を確認し、現場ヒアリングや追加計測につなげる一次診断レポートを作れることです。相関は原因を証明するものではなく、優先して確認する仮説を絞る道具として扱います。

現場でよくある状況

  • 複数ラインの日報を統合したが、単位・入力ルール・欠損理由が揃っていない
  • 全工場平均だけが共有され、製品構成やライン差が見えない
  • 「温度が高い日に不良が多い」など、経験則とデータの関係が未検証
  • 分析担当と現場で、停止時間や良品数の定義が異なる

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

不良率は設備条件だけでなく、製品難易度、ライン、ロット、検査条件などの影響を同時に受けます。また、欠損がランダムとは限りません。たとえば振動センサーの欠測が設備停止時に集中すれば、欠損行の単純除外は異常を過小評価します。したがって、粒度・型・欠損・分布・層別を順に確認する必要があります。

今回扱うノックの全体像

No.操作実務上の確認事項
001CSVを読み込む文字コード・区切り・行粒度
002先頭・末尾・列名期間、並び、想定列
003行数・列数観測網羅性、粒度
004データ型計算・時系列処理の可否
005欠損値欠測の規模と業務上の理由
006基本統計量水準、ばらつき、異常候補
007カテゴリ集計ライン・製品構成の偏り
008ヒストグラム分布形状、裾、複数集団
009散布図変数間の関係と層別差
010相関係数仮説の優先順位付け

Python環境の準備

pandas で表形式データを扱い、numpy で再現可能な架空データを生成し、matplotlib で可視化します。乱数シードを固定することで、再実行しても同じ結果になります。

from io import StringIO
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt

SEED = 1201
rng = np.random.default_rng(SEED)
plt.rcParams["figure.figsize"] = (8, 4.5)

print(f"Python: {sys.version.split()[0]}")
print(f"pandas: {pd.__version__}")
print(f"matplotlib: {matplotlib.__version__}")
print(f"乱数シード: {SEED}")
Python: 3.13.1
pandas: 3.0.3
matplotlib: 3.11.0
乱数シード: 1201

架空データの作成

1行を「ある日・あるライン・ある製品の日次実績」とします。分析単位を明示することは重要です。同じ列でも1行が設備、ロット、時間帯のどれを表すかで、集計結果の意味が変わるためです。

不良率は次式で定義します。

\text{不良率(%)}=\frac{\text{不良数}}{\text{生産数}}\times 100

温度・振動・停止時間が上がると不良がやや増え、製品P-300は加工難度が高い、という架空の構造を入れます。振動値には意図的に少数の欠損を作ります。

n_days = 60
dates = pd.date_range("2025-04-01", periods=n_days, freq="D")
lines = np.array(["L-A", "L-B", "L-C"])
products = np.array(["P-100", "P-200", "P-300"])
records = []

for date in dates:
    for line in lines:
        product = rng.choice(products, p=[0.45, 0.35, 0.20])
        planned = int(rng.integers(430, 571))
        downtime = max(0, rng.gamma(2.0, 8.0) + (line == "L-C") * 6)
        produced = max(300, int(planned - 1.25 * downtime + rng.normal(0, 10)))
        temperature = rng.normal(72 + (line == "L-C") * 2.5, 3.5)
        vibration = rng.normal(2.6 + (line == "L-B") * 0.20, 0.38)
        defect_rate = (0.010 + 0.0006 * downtime + 0.0018 * (temperature - 72)
                       + 0.009 * (vibration - 2.6) + 0.010 * (product == "P-300"))
        defect_rate = float(np.clip(defect_rate, 0.002, 0.12))
        defects = int(rng.binomial(produced, defect_rate))
        records.append([date, line, product, planned, produced, downtime,
                        temperature, vibration, defects])

source_df = pd.DataFrame(records, columns=[
    "date", "line", "product", "planned_qty", "produced_qty",
    "downtime_min", "temperature_c", "vibration_mm_s", "defect_qty"
])
source_df.loc[rng.choice(source_df.index, 7, replace=False), "vibration_mm_s"] = np.nan
source_df["defect_rate_pct"] = source_df["defect_qty"] / source_df["produced_qty"] * 100
source_df["downtime_min"] = source_df["downtime_min"].round(1)
source_df["temperature_c"] = source_df["temperature_c"].round(1)
source_df["vibration_mm_s"] = source_df["vibration_mm_s"].round(2)
source_df["defect_rate_pct"] = source_df["defect_rate_pct"].round(2)

# 外部ファイルに依存しないよう、CSV文字列をメモリ上に作る
csv_buffer = StringIO()
source_df.to_csv(csv_buffer, index=False)
print(f"作成した架空データ: {len(source_df):,}行 × {source_df.shape[1]}列")
source_df.head(3)
作成した架空データ: 180行 × 10列
date line product planned_qty produced_qty downtime_min temperature_c vibration_mm_s defect_qty defect_rate_pct
0 2025-04-01 L-A P-100 431 412 13.1 78.9 2.42 22 5.34
1 2025-04-01 L-B P-100 457 435 17.7 72.1 3.18 11 2.53
2 2025-04-01 L-C P-200 460 421 16.7 74.8 2.57 9 2.14

No.001:CSVを読み込む

実務での意味

CSVの受入は分析の入口です。読み込みに成功しただけでなく、1行の粒度、文字コード、区切り文字、日付の解釈がデータ仕様と一致しているかを確認します。自動連携では、想定列が欠けた場合に処理を止める検証も必要です。

分析・モデル化の考え方

ここではメモリ上のCSVを pd.read_csv で読み込み、parse_dates で日付列を日時型へ変換します。入力と分析処理を分けることで、将来ファイル保存やデータベース連携へ置き換えやすくなります。

Pythonで確認する

csv_buffer.seek(0)
df = pd.read_csv(csv_buffer, parse_dates=["date"])
print(f"読込完了: {len(df):,}行 × {df.shape[1]}列")
df.head(3)
読込完了: 180行 × 10列
date line product planned_qty produced_qty downtime_min temperature_c vibration_mm_s defect_qty defect_rate_pct
0 2025-04-01 L-A P-100 431 412 13.1 78.9 2.42 22 5.34
1 2025-04-01 L-B P-100 457 435 17.7 72.1 3.18 11 2.53
2 2025-04-01 L-C P-200 460 421 16.7 74.8 2.57 9 2.14

結果の読み取り

180行が読み込まれ、日付・ライン・製品ごとの記録を確認できます。実務ではこの時点で、期待行数や必須列との照合を自動化すると、上流システムの仕様変更を早期に検知できます。

No.002:データの先頭・末尾・列名を確認する

実務での意味

先頭と末尾は、対象期間や並び順の取り違えを発見する簡便な検査です。列名の一覧は、現場のデータ辞書と分析コードを接続する契約になります。

分析・モデル化の考え方

headtailcolumns を組み合わせます。ただし端だけでは途中の異常を保証できないため、これは完全検査ではなく受入時の第一関門です。

Pythonで確認する

print("列名:", df.columns.tolist())
print("\n先頭2行")
display(df.head(2))
print("末尾2行")
display(df.tail(2))
列名: ['date', 'line', 'product', 'planned_qty', 'produced_qty', 'downtime_min', 'temperature_c', 'vibration_mm_s', 'defect_qty', 'defect_rate_pct']

先頭2行
date line product planned_qty produced_qty downtime_min temperature_c vibration_mm_s defect_qty defect_rate_pct
0 2025-04-01 L-A P-100 431 412 13.1 78.9 2.42 22 5.34
1 2025-04-01 L-B P-100 457 435 17.7 72.1 3.18 11 2.53
末尾2行
date line product planned_qty produced_qty downtime_min temperature_c vibration_mm_s defect_qty defect_rate_pct
178 2025-05-30 L-B P-200 455 430 10.3 77.8 3.25 10 2.33
179 2025-05-30 L-C P-200 460 435 12.9 73.0 2.82 8 1.84

結果の読み取り

期間は2025年4月1日から5月30日までで、必要な生産・設備・品質列が存在します。日付順であることも見えますが、本番では date.is_monotonic_increasing や必須列集合による機械的検証を追加します。

No.003:データの行数・列数を確認する

実務での意味

件数は観測網羅性を表す基本KPIです。60日×3ラインなら期待値は180行です。少なければ未提出、多ければ重複の可能性があり、分析結果以前に業務プロセスを確認すべきです。

分析・モデル化の考え方

shape で行列数を確認し、業務上の期待件数との差を計算します。さらに日付×ラインの重複を数えることで、粒度の一意性も検証します。

Pythonで確認する

expected_rows = n_days * len(lines)
actual_rows, actual_cols = df.shape
duplicate_keys = df.duplicated(["date", "line"]).sum()
print(f"実績: {actual_rows}行 × {actual_cols}列")
print(f"期待行数との差: {actual_rows - expected_rows:+d}行")
print(f"日付×ラインの重複: {duplicate_keys}行")
実績: 180行 × 10列
期待行数との差: +0行
日付×ラインの重複: 0行

結果の読み取り

期待値との差とキー重複はいずれも0です。したがって今回のデータは日次・ライン単位で網羅されています。ただし休業日がある工場では、操業カレンダーを使って期待件数を定義する必要があります。

No.004:データ型を確認する

実務での意味

数量を文字列のまま集計すると結合になり、日付を文字列のまま扱うと月次集計や期間差が誤ります。型は計算可能性だけでなく、値の意味を守る品質条件です。

分析・モデル化の考え方

dtypesinfo で型、非欠損件数、メモリ使用量を確認します。日付は datetime、識別子は object、測定値と数量は数値型であることを期待します。

Pythonで確認する

dtype_table = pd.DataFrame({"dtype": df.dtypes.astype(str), "non_null": df.notna().sum()})
display(dtype_table)
print(f"date列は日時型: {pd.api.types.is_datetime64_any_dtype(df['date'])}")
dtype non_null
date datetime64[us] 180
line str 180
product str 180
planned_qty int64 180
produced_qty int64 180
downtime_min float64 180
temperature_c float64 180
vibration_mm_s float64 173
defect_qty int64 180
defect_rate_pct float64 180
date列は日時型: True

結果の読み取り

日付は日時型、数量は整数、連続測定値は浮動小数点です。振動だけ非欠損件数が少ないことも型表から分かります。型確認と欠損確認を一緒に見ると、入力異常を早く発見できます。

No.005:欠損値の有無を確認する

実務での意味

センサー欠測は単なる空欄ではなく、通信断、校正、停止中など設備状態の手掛かりになり得ます。欠損率と業務上の発生理由を確認せず、ゼロ補完や行削除をしてはいけません。

分析・モデル化の考え方

各列の欠損件数と割合を計算します。割合は mj/nimes100m_j/n imes100mjm_j は列 jj の欠損数、nn は総行数)です。後続分析では、対象変数ごとに利用可能件数が変わる点にも注意します。

Pythonで確認する

missing = pd.DataFrame({"missing_count": df.isna().sum(), "missing_rate_pct": df.isna().mean().mul(100).round(2)})
missing = missing.query("missing_count > 0")
display(missing if not missing.empty else pd.DataFrame({"status": ["欠損なし"]}))
missing_count missing_rate_pct
vibration_mm_s 7 3.89

結果の読み取り

振動値に7件(約3.9%)の欠損があります。規模は小さくても、特定ラインや高停止時間帯への偏りを調べるべきです。本記事では削除・補完をせず、グラフや相関計算時に利用可能な行を使います。

No.006:基本統計量を確認する

実務での意味

平均だけでは、設備条件のばらつきや極端な停止を見落とします。中央値、四分位点、最小・最大を併記すると、通常操業レンジと調査候補を把握できます。

分析・モデル化の考え方

describe は件数、平均、標準偏差、四分位点を返します。標準偏差 ss は観測の散らばりを表しますが、単位が異なる列同士の大小を直接比較しない点に注意します。

Pythonで確認する

numeric_cols = ["produced_qty", "downtime_min", "temperature_c", "vibration_mm_s", "defect_rate_pct"]
stats = df[numeric_cols].describe().T.round(2)
display(stats)
print(f"不良率中央値: {df['defect_rate_pct'].median():.2f}%")
count mean std min 25% 50% 75% max
produced_qty 180.0 474.74 40.42 372.00 444.00 474.00 508.00 574.00
downtime_min 180.0 19.11 11.63 1.40 10.30 17.95 26.02 63.10
temperature_c 180.0 72.89 3.72 59.10 70.70 73.00 75.30 81.00
vibration_mm_s 173.0 2.64 0.41 1.69 2.36 2.63 2.91 3.81
defect_rate_pct 180.0 2.55 1.38 0.00 1.53 2.37 3.29 6.11
不良率中央値: 2.37%

結果の読み取り

停止時間や不良率は最大値が第3四分位点より大きく、右裾の長い可能性があります。最大日の即時調査候補にはなりますが、外れ値と断定するには保全記録や製品条件との照合が必要です。

No.007:カテゴリ変数を集計する

実務での意味

ライン・製品構成が偏ると、全体平均の変化が設備改善ではなく生産ミックスの変化で起きることがあります。カテゴリ別の件数とKPIを並べ、比較可能性を確認します。

分析・モデル化の考え方

groupby でライン別の観測数、生産数、停止時間、不良率を集計します。不良率は日別率の単純平均ではなく、総不良数÷総生産数で加重して計算します。

Pythonで確認する

line_summary = (df.groupby("line", as_index=False)
 .agg(records=("date", "size"), produced_qty=("produced_qty", "sum"),
      defect_qty=("defect_qty", "sum"), avg_downtime_min=("downtime_min", "mean")))
line_summary["weighted_defect_rate_pct"] = line_summary["defect_qty"] / line_summary["produced_qty"] * 100
display(line_summary.round(2))
print("製品別件数:")
display(df["product"].value_counts().rename_axis("product").to_frame("records"))
line records produced_qty defect_qty avg_downtime_min weighted_defect_rate_pct
0 L-A 60 28345 638 16.64 2.25
1 L-B 60 28781 665 18.37 2.31
2 L-C 60 28327 854 22.31 3.01
製品別件数:
records
product
P-100 82
P-200 56
P-300 42

結果の読み取り

各ラインは60件ずつで観測数は均等ですが、ラインCは停止時間と加重不良率が相対的に高めです。これは原因確定ではなく、製品構成や設備条件を層別して確認する優先順位を示します。

No.008:数値変数をヒストグラムで可視化する

実務での意味

分布を見ると、平均値だけでは分からない裾の長さ、山の数、管理限界付近への集中が見えます。停止時間の長い日が少数ある場合、保全計画は平均停止時間だけで設計できません。

分析・モデル化の考え方

ヒストグラムは値域を区間(bin)に分けて頻度を数えます。bin数で見え方が変わるため、複数設定や箱ひげ図との併用が有効です。ここでは不良率の分布と平均・中央値を示します。

Pythonで確認する

fig, ax = plt.subplots()
ax.hist(df["defect_rate_pct"], bins=14, color="#2878B5", edgecolor="white", alpha=0.85)
ax.axvline(df["defect_rate_pct"].mean(), color="#D95319", linestyle="--", label="Mean")
ax.axvline(df["defect_rate_pct"].median(), color="#2CA02C", linestyle=":", label="Median")
ax.set_title("Distribution of Daily Defect Rate")
ax.set_xlabel("Defect rate (%)")
ax.set_ylabel("Number of line-days")
ax.grid(axis="y", alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()

png

結果の読み取り

不良率は低い領域に多く、右側に裾が伸びます。したがって平均だけを代表値とせず、中央値や上位分位点も品質会議で共有すると、少数の悪化日の影響を説明しやすくなります。

No.009:散布図で変数同士の関係を見る

実務での意味

停止時間と不良率の同時上昇が見えれば、段取り・再起動・設備不調を優先調査する仮説になります。点をライン別に色分けすると、全体傾向と設備群の違いを混同しにくくなります。

分析・モデル化の考え方

散布図の各点は1ライン日です。横軸に停止時間、縦軸に不良率を置き、ラインで層別します。関係が曲線的か、分散が変わるか、特殊な点があるかも確認します。

Pythonで確認する

fig, ax = plt.subplots()
colors = {"L-A": "#2878B5", "L-B": "#F39C12", "L-C": "#3A923A"}
for line, part in df.groupby("line"):
    ax.scatter(part["downtime_min"], part["defect_rate_pct"], s=35, alpha=0.7, label=line, color=colors[line])
ax.set_title("Downtime and Daily Defect Rate by Line")
ax.set_xlabel("Downtime (minutes)")
ax.set_ylabel("Defect rate (%)")
ax.grid(alpha=0.3)
ax.legend(title="Line")
plt.tight_layout()
plt.show()

png

結果の読み取り

停止時間が長いほど不良率が高い傾向が見え、ラインCには右上の点が比較的多くあります。ただし製品P-300の比率など第三の要因もあり得るため、散布図だけで停止が不良の原因とは結論づけません。

No.010:相関係数を確認する

実務での意味

相関係数は多数の測定値から関連の強い組合せを短時間で探索し、現場確認の順序を決めるのに役立ちます。一方、因果関係、非線形関係、ライン内の関係を自動的に保証しません。

分析・モデル化の考え方

Pearsonの相関係数は共分散を標準偏差で規格化した値です。rxy=cov(x,y)/(sxsy)r_{xy}=\mathrm{cov}(x,y)/(s_xs_y) で、-1から1を取ります。欠損はペアごとに除外されます。

Pythonで確認する

corr_cols = ["produced_qty", "downtime_min", "temperature_c", "vibration_mm_s", "defect_rate_pct"]
corr = df[corr_cols].corr().round(2)
display(corr)

fig, ax = plt.subplots(figsize=(7, 5.5))
im = ax.imshow(corr, cmap="coolwarm", vmin=-1, vmax=1)
ax.set_xticks(range(len(corr_cols)), corr_cols, rotation=35, ha="right")
ax.set_yticks(range(len(corr_cols)), corr_cols)
for i in range(len(corr_cols)):
    for j in range(len(corr_cols)):
        ax.text(j, i, f"{corr.iloc[i, j]:.2f}", ha="center", va="center", fontsize=9)
ax.set_title("Pearson Correlation Matrix")
ax.set_xlabel("Variables")
ax.set_ylabel("Variables")
ax.grid(False)
fig.colorbar(im, ax=ax, label="Correlation coefficient")
plt.tight_layout()
plt.show()
produced_qty downtime_min temperature_c vibration_mm_s defect_rate_pct
produced_qty 1.00 -0.20 -0.18 0.04 -0.23
downtime_min -0.20 1.00 0.13 -0.03 0.60
temperature_c -0.18 0.13 1.00 -0.08 0.57
vibration_mm_s 0.04 -0.03 -0.08 1.00 0.13
defect_rate_pct -0.23 0.60 0.57 0.13 1.00

png

結果の読み取り

不良率は停止時間・温度などと正の相関を示し、生産数とは負の相関を示します。これは架空データに組み込んだ構造と整合しますが、次の分析では製品・ラインを統制した回帰や時系列順の検証が必要です。

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

今回の一次診断では、データ件数とキー粒度は期待どおりで、振動値に少数の欠損があり、ラインCの停止時間と不良率が相対的に高いことが分かりました。また、不良率は左右対称ではないため、平均値だけでなく中央値・上位分位点を併用すべきです。

重要なのは、これらを「ラインCが原因」「停止を減らせば必ず不良が減る」という結論にしないことです。ここで得たのは、保全履歴、製品ミックス、段取り、作業班、測定校正を次に照合するための仮説です。記述統計は、モデル構築前の儀式ではなく、分析の前提と意思決定リスクを明らかにする工程です。

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

  1. データ定義書:1行の粒度、単位、計算式、責任部署を明記する
  2. 自動受入検査:必須列、型、件数、重複、値域、鮮度をチェックする
  3. 欠損理由コード:通信断・保全・非稼働・入力漏れを区別する
  4. 層別設計:ライン、製品、設備、作業班、ロットを追跡可能にする
  5. 判断ルール:誰がどの閾値で調査・停止・改善を判断するかを決める
  6. 効果検証:改善前後を同じKPIと比較可能な条件で評価する

個人情報や営業秘密が含まれる場合は、アクセス制御、匿名化、保存期間、監査ログも設計対象です。Notebookでの探索結果をそのまま本番運用にせず、定期実行、例外通知、版管理を含む処理へ移します。

まとめ

No.001〜No.010では、CSVを読み込み、構造・型・欠損・統計量・カテゴリ構成・分布・変数間関係を確認しました。この順序を守ることで、入力不備や集計の錯覚を減らし、機械学習へ進む前に「何を追加確認すべきか」を説明できます。

次の実務ステップは、振動欠損の発生理由を調べ、ライン・製品別に関係を層別し、保全履歴と照合することです。モデル精度より先に、データの意味と現場の判断を接続することが、継続利用される分析の土台になります。

法人向けのご相談

数理工房では、製造データの棚卸し、品質・設備KPIの設計、PoC、予測・異常検知モデルの構築、現場定着まで、課題とデータ成熟度に合わせてご支援します。「データはあるが何から確認すべきか」「分析結果が改善活動につながらない」といった段階からご相談いただけます。

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