100本ノック / Python / データ分析のためのPython入門100本ノック
コードセルを分けて実行する
EV 部品工場の品質検査データを Polars で前処理する
100本ノック 第9章(No.081〜No.090):Polarsによるデータ加工
本記事は「データ分析のための Python 入門 100本ノック」シリーズの 第9章 です。
第8章(No.071〜080)では Polars の基本操作(DataFrame の読み込み・構造確認・基本統計量)を学びました。
本章では 実務で必須のデータ加工操作(列選択・行フィルタ・列追加・欠損値処理・並び替え・重複除去)を、
EV(電気自動車)向け電子部品工場の品質検査データを題材に習得します。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
はじめに:この記事で扱う製造業の実務課題
EV 部品メーカーの品質管理チームが直面している問題です。
現状
1. 全国 5 工場から毎日の品質検査データが CSV で届き、結合すると 315 件あった
2. よく見ると同一レコードが 15 件重複している(工場側のデータ同期バグ)
3. センサー故障で不良数が記録されていない行が 30 件ある
4. 検査員がシステムにログインしなかった日は inspector_id が空欄(45 件)
5. factory_name 列は factory_id と 1 対 1 対応で冗長——片方は不要
6. raw_note 列はテスト用メモで本番分析では使わない
Polars の 10 個のデータ加工操作を組み合わせることで、こうした生データの品質問題を
体系的に整理・クレンジングし、分析可能な状態に整えられます。
現場でよくある状況
| 場面 | 課題 |
|---|---|
| 多列データの取り回し | 収集したデータに不要な列が多く、確認・加工が煩雑 |
| 条件フィルタリング | 「不良率 > 2%」「特定工場のみ」の条件抽出を毎回 Excel で手作業 |
| KPI 列の追加 | 良品数・売上・損失を Excel の数式で毎回手動計算している |
| 列名の英語表記 | システム出力が英語列名で、報告書向けに毎回手動で変更している |
| 欠損値の存在確認 | どの列に何件の欠損があるかを把握できていない |
| 欠損値による集計エラー | NULL を含む列を集計すると結果がおかしくなる |
| 重複データ | データ同期バグで同一レコードが 2 回登録されていることに気づかない |
| ソート・ランキング | 不良率ワーストランキングを毎回手作業で並び替えて確認している |
Polars の 10 のデータ加工操作をマスターすることで、これらを パイプライン化 できます。
なぜこの問題は判断が難しいのか
Polars のデータ加工は直感的に見えますが、製造データの文脈では次の注意点があります。
-
selectvsdropの使い分け
「残したい列が少ない場合はselect」「削除したい列が少ない場合はdrop」が基本方針です。
列数が多い実務データではdropの方が保守性が高いことが多いです -
filterの論理演算
Polars では&(AND)と|(OR)を使います。Python のand/orは使えません。
複数条件は必ず丸括弧()で囲む必要があります
例:df.filter((pl.col("a") > 2) & (pl.col("b") == "X")) -
with_columnsの同時評価
with_columns内の複数の式は 元の DataFrame を基準に同時評価 されます。
「列 A の計算結果を列 B の式で使いたい」場合はwith_columnsを2回呼ぶか
チェーン(.with_columns(...).with_columns(...))を使います -
欠損値処理の順序が重要
「欠損値を何で埋めるか」を業務ルールとして決めてから操作します。
「0 で埋める」「前後の値で埋める」「削除する」は
分析の前提条件に直結するため、コメントで明示してください -
uniqueのkeep戦略
重複除去の際、keep="first"とkeep="last"では残すレコードが変わります。
タイムスタンプ順に並んでいる場合はkeep="first"が「古いレコード優先」です
今回扱うノックの全体像
| No. | タイトル | 主な Polars 操作 |
|---|---|---|
| 081 | 特定の列を選択する | df.select(["col1", "col2", ...]) |
| 082 | 条件に合う行を抽出する | df.filter(pl.col("col") > value) |
| 083 | 列を追加する | df.with_columns([expr.alias("name"), ...]) |
| 084 | 列名を変更する | df.rename({"old_name": "new_name"}) |
| 085 | 不要な列を削除する | df.drop(["col1", "col2"]) |
| 086 | 欠損値を確認する | df.null_count() |
| 087 | 欠損値を削除する | df.drop_nulls(subset=["col"]) |
| 088 | 欠損値を補完する | df.with_columns(pl.col("col").fill_null(value)) |
| 089 | データを並び替える | df.sort("col", descending=True) |
| 090 | 重複データを削除する | df.unique(subset=["col1", ...], keep="first") |
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 polars as pl
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" Polars : {pl.__version__}")
print(f" Matplotlib: {matplotlib.__version__}")
ライブラリ読み込み完了
NumPy : 2.5.1
Polars : 1.42.1
Matplotlib: 3.11.0
架空データの作成
想定シナリオ: EV(電気自動車)向け電子部品メーカー / 品質管理チーム
期間: 2024年1月(第1〜4週 月〜金 20営業日)
管理対象: 5工場 × 3製造ライン = 15 レコード / 日
件数: 300 件(20日 × 15)+ 重複 15 件 = 315 件
| 列名 | 内容 | データ品質 |
|---|---|---|
record_id | レコードID | 完全 |
factory_id | 工場ID(F001〜F005) | 完全 |
factory_name | 工場名(factory_id と 1:1 対応) | 完全(冗長) |
line_id | ライン番号(L-A/L-B/L-C) | 完全 |
inspection_date | 検査日(Date 型) | 完全 |
product_code | 製品コード | 完全 |
unit_price | 単価(円) | 完全 |
production | 日次生産数 | 完全 |
defect_count | 不良数(センサー故障で一部欠損) | 30 件 NULL |
defect_rate | 不良率(%)(defect_count に連動) | 30 件 NULL |
inspector_id | 検査員ID(ログイン未記録で一部欠損) | 45 件 NULL |
raw_note | テスト用メモ(分析では不要) | 完全(不要) |
np.random.seed(42)
# ── 設定 ────────────────────────────────────────────────────────────────
FACTORY_IDS = ["F001", "F002", "F003", "F004", "F005"]
FACTORY_NAMES = {
"F001": "東京工場", "F002": "大阪工場", "F003": "名古屋工場",
"F004": "横浜工場", "F005": "福岡工場",
}
FACTORY_COLORS = {
"F001": "#4C72B0", "F002": "#DD8452", "F003": "#55A868",
"F004": "#C44E52", "F005": "#8172B2",
}
LINE_IDS = ["L-A", "L-B", "L-C"]
PRODUCTS = ["EV-PCB-001", "EV-PCB-002", "EV-PCB-003"]
UNIT_PRICES = {"EV-PCB-001": 1800, "EV-PCB-002": 2400, "EV-PCB-003": 3600}
BASE_PROD = {"F001": 480, "F002": 450, "F003": 510, "F004": 420, "F005": 380}
BASE_DR = {"F001": 0.018, "F002": 0.015, "F003": 0.023, "F004": 0.027, "F005": 0.020}
INSPECTORS = ["INS-001", "INS-002", "INS-003", "INS-004", "INS-005"]
N_DAYS = 20
BASE_DATE = datetime.date(2024, 1, 4)
# ── レコード生成(5工場 × 3ライン × 20日 = 300件)────────────────────────
rows = []
for day_idx in range(N_DAYS):
check_date = BASE_DATE + datetime.timedelta(days=day_idx)
for fid in FACTORY_IDS:
for lid in LINE_IDS:
prod_code = str(np.random.choice(PRODUCTS))
unit_price = UNIT_PRICES[prod_code]
production = int(BASE_PROD[fid] * np.random.uniform(0.92, 1.08))
dr = BASE_DR[fid] * np.random.uniform(0.7, 1.4)
defect_cnt = max(0, int(round(production * dr + np.random.randn() * 1.5)))
defect_rate = round(defect_cnt / production * 100, 4)
inspector = str(np.random.choice(INSPECTORS))
note_opts = ["normal_check", "spot_check", "recheck", "", ""]
raw_note = str(np.random.choice(note_opts))
rec_no = day_idx * 15 + FACTORY_IDS.index(fid) * 3 + LINE_IDS.index(lid)
rows.append({
"record_id": f"REC-{rec_no:04d}",
"factory_id": fid,
"factory_name": FACTORY_NAMES[fid],
"line_id": lid,
"inspection_date": check_date,
"product_code": prod_code,
"unit_price": unit_price,
"production": production,
"defect_count": defect_cnt,
"defect_rate": defect_rate,
"inspector_id": inspector,
"raw_note": raw_note,
})
# ── NULL を導入(センサー欠損・ログイン未記録)──────────────────────────────
null_defect_idx = np.random.choice(len(rows), size=30, replace=False)
null_inspector_idx = np.random.choice(len(rows), size=45, replace=False)
for idx in null_defect_idx:
rows[idx]["defect_count"] = None
rows[idx]["defect_rate"] = None
for idx in null_inspector_idx:
rows[idx]["inspector_id"] = None
# ── 重複レコードを追加(データ同期バグ:15件)─────────────────────────────
dup_idx = np.random.choice(len(rows), size=15, replace=False)
for i in dup_idx:
rows.append(dict(rows[i]))
# ── Polars DataFrame 作成 ────────────────────────────────────────────────
df_raw = pl.DataFrame(rows)
print(f"総レコード数 : {len(df_raw):,} 件(300 件 + 重複 15 件)")
print(f"列数 : {df_raw.width} 列")
print(f"列名 : {df_raw.columns}")
print()
print(df_raw.head(5))
総レコード数 : 315 件(300 件 + 重複 15 件)
列数 : 12 列
列名 : ['record_id', 'factory_id', 'factory_name', 'line_id', 'inspection_date', 'product_code', 'unit_price', 'production', 'defect_count', 'defect_rate', 'inspector_id', 'raw_note']
shape: (5, 12)
┌───────────┬────────────┬───────────┬─────────┬───┬───────────┬───────────┬───────────┬───────────┐
│ record_id ┆ factory_id ┆ factory_n ┆ line_id ┆ … ┆ defect_co ┆ defect_ra ┆ inspector ┆ raw_note │
│ --- ┆ --- ┆ ame ┆ --- ┆ ┆ unt ┆ te ┆ _id ┆ --- │
│ str ┆ str ┆ --- ┆ str ┆ ┆ --- ┆ --- ┆ --- ┆ str │
│ ┆ ┆ str ┆ ┆ ┆ i64 ┆ f64 ┆ str ┆ │
╞═══════════╪════════════╪═══════════╪═════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡
│ REC-0000 ┆ F001 ┆ 東京工場 ┆ L-A ┆ … ┆ 8 ┆ 1.5936 ┆ null ┆ recheck │
│ REC-0001 ┆ F001 ┆ 東京工場 ┆ L-B ┆ … ┆ 13 ┆ 2.9148 ┆ INS-004 ┆ recheck │
│ REC-0002 ┆ F001 ┆ 東京工場 ┆ L-C ┆ … ┆ 10 ┆ 2.2472 ┆ INS-002 ┆ │
│ REC-0003 ┆ F002 ┆ 大阪工場 ┆ L-A ┆ … ┆ 8 ┆ 1.7937 ┆ INS-003 ┆ │
│ REC-0004 ┆ F002 ┆ 大阪工場 ┆ L-B ┆ … ┆ 8 ┆ 1.6529 ┆ INS-001 ┆ spot_chec │
│ ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ k │
└───────────┴────────────┴───────────┴─────────┴───┴───────────┴───────────┴───────────┴───────────┘
No.081:特定の列を選択する
実務での意味
品質検査データには 12 列ありますが、分析に使うのは「工場・ライン・日付・製品・生産数・不良率」の
6 列だけというケースがよくあります。select() で必要な列だけを取り出すことで、
後続の処理が高速・シンプルになります。
報告書の出力対象列を絞り込むことで、閲覧者が本質的な情報に集中できます。
分析・モデル化の考え方
select() は 射影(Projection) と呼ばれるリレーショナル代数の操作です。
列数を削減することで DataFrame のメモリ使用量を減らし、
後続の filter や group_by のパフォーマンスも向上します。
select に式(pl.col("col").alias("new_name"))を渡すと
列選択と変換を同時に行えます。
Python で確認する
# 分析に必要な 6 列だけを選択する
df_select = df_raw.select([
"factory_id",
"line_id",
"inspection_date",
"product_code",
"production",
"defect_rate",
])
print(f"元の列数 : {df_raw.width} 列 {df_raw.columns}")
print()
print(f"選択後 : {df_select.width} 列 {df_select.columns}")
print()
print(df_select.head(8))
print()
# 式を使った select(列選択 + 変換を同時に実行)
df_expr_select = df_raw.select([
"factory_id",
"line_id",
(pl.col("defect_rate") / 100).alias("defect_rate_decimal"), # % → 小数
pl.col("production").alias("daily_production"),
])
print("=== 式を使った select(不良率を小数に変換しながら選択)===")
print(df_expr_select.head(5))
元の列数 : 12 列 ['record_id', 'factory_id', 'factory_name', 'line_id', 'inspection_date', 'product_code', 'unit_price', 'production', 'defect_count', 'defect_rate', 'inspector_id', 'raw_note']
選択後 : 6 列 ['factory_id', 'line_id', 'inspection_date', 'product_code', 'production', 'defect_rate']
shape: (8, 6)
┌────────────┬─────────┬─────────────────┬──────────────┬────────────┬─────────────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ product_code ┆ production ┆ defect_rate │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ str ┆ i64 ┆ f64 │
╞════════════╪═════════╪═════════════════╪══════════════╪════════════╪═════════════╡
│ F001 ┆ L-A ┆ 2024-01-04 ┆ EV-PCB-003 ┆ 502 ┆ 1.5936 │
│ F001 ┆ L-B ┆ 2024-01-04 ┆ EV-PCB-003 ┆ 446 ┆ 2.9148 │
│ F001 ┆ L-C ┆ 2024-01-04 ┆ EV-PCB-002 ┆ 445 ┆ 2.2472 │
│ F002 ┆ L-A ┆ 2024-01-04 ┆ EV-PCB-003 ┆ 446 ┆ 1.7937 │
│ F002 ┆ L-B ┆ 2024-01-04 ┆ EV-PCB-003 ┆ 484 ┆ 1.6529 │
│ F002 ┆ L-C ┆ 2024-01-04 ┆ EV-PCB-001 ┆ 483 ┆ null │
│ F003 ┆ L-A ┆ 2024-01-04 ┆ EV-PCB-001 ┆ 488 ┆ 2.2541 │
│ F003 ┆ L-B ┆ 2024-01-04 ┆ EV-PCB-002 ┆ 523 ┆ 2.4857 │
└────────────┴─────────┴─────────────────┴──────────────┴────────────┴─────────────┘
=== 式を使った select(不良率を小数に変換しながら選択)===
shape: (5, 4)
┌────────────┬─────────┬─────────────────────┬──────────────────┐
│ factory_id ┆ line_id ┆ defect_rate_decimal ┆ daily_production │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ f64 ┆ i64 │
╞════════════╪═════════╪═════════════════════╪══════════════════╡
│ F001 ┆ L-A ┆ 0.015936 ┆ 502 │
│ F001 ┆ L-B ┆ 0.029148 ┆ 446 │
│ F001 ┆ L-C ┆ 0.022472 ┆ 445 │
│ F002 ┆ L-A ┆ 0.017937 ┆ 446 │
│ F002 ┆ L-B ┆ 0.016529 ┆ 484 │
└────────────┴─────────┴─────────────────────┴──────────────────┘
結果の読み取り
select() は元の DataFrame から指定した列だけを含む新しい DataFrame を返します。
df_raw の 12 列が 6 列に絞り込まれ、分析に必要なデータだけが残りました。
pl.col("defect_rate") / 100 のように列選択と同時に変換も可能で、
「%表示」の列を「小数表示」に変換しながら選択する実務的なユースケースに使えます。
「列が多くて処理が遅い」と感じた場合、select() での列削減が
パフォーマンス改善の最初の施策になります。
No.082:条件に合う行を抽出する
実務での意味
「不良率が 2.5% を超えた日を抽出する」「横浜工場(F004)のデータだけを確認する」
という条件フィルタリングは、品質管理レポートで最も頻繁に使う操作です。
filter() を使えば、SQL の WHERE 句に相当する行絞り込みを 1 行で実行できます。
分析・モデル化の考え方
filter() は 選択(Selection) と呼ばれるリレーショナル代数の操作です。
Polars では &(AND)と |(OR)で複合条件を記述します。
また、pl.col("col").is_null() / is_not_null() で欠損値の有無を条件にできます。
複数条件は必ず各条件を丸括弧で囲んでください。
Python で確認する
# 単一条件:不良率が 2.5% 超の行を抽出
df_high_dr = df_raw.filter(pl.col("defect_rate") > 2.5)
print(f"不良率 > 2.5%: {len(df_high_dr):,} 件 / {len(df_raw):,} 件 "
f"({len(df_high_dr)/len(df_raw)*100:.1f}%)")
print(df_high_dr.select(["factory_id", "line_id", "inspection_date",
"production", "defect_count", "defect_rate"]).head(5))
print()
# 複合条件:横浜工場(F004)かつ不良率 > 2.0% の行
df_f004_high = df_raw.filter(
(pl.col("factory_id") == "F004") & (pl.col("defect_rate") > 2.0)
)
print(f"=== F004-横浜 かつ 不良率 > 2.0% ===")
print(f" 件数: {len(df_f004_high)} 件")
print(df_f004_high.select(["factory_id", "line_id", "inspection_date",
"production", "defect_count", "defect_rate"]).head(5))
print()
# NULL を含む行の抽出(is_null)
df_missing = df_raw.filter(pl.col("defect_count").is_null())
print(f"=== defect_count が欠損している行 ===")
print(f" 件数: {len(df_missing)} 件")
print(df_missing.select(["factory_id", "line_id", "inspection_date",
"defect_count", "defect_rate", "inspector_id"]).head(5))
不良率 > 2.5%: 86 件 / 315 件 (27.3%)
shape: (5, 6)
┌────────────┬─────────┬─────────────────┬────────────┬──────────────┬─────────────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ production ┆ defect_count ┆ defect_rate │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ i64 ┆ i64 ┆ f64 │
╞════════════╪═════════╪═════════════════╪════════════╪══════════════╪═════════════╡
│ F001 ┆ L-B ┆ 2024-01-04 ┆ 446 ┆ 13 ┆ 2.9148 │
│ F004 ┆ L-A ┆ 2024-01-04 ┆ 450 ┆ 14 ┆ 3.1111 │
│ F004 ┆ L-B ┆ 2024-01-04 ┆ 442 ┆ 12 ┆ 2.7149 │
│ F005 ┆ L-A ┆ 2024-01-04 ┆ 399 ┆ 11 ┆ 2.7569 │
│ F005 ┆ L-B ┆ 2024-01-04 ┆ 389 ┆ 12 ┆ 3.0848 │
└────────────┴─────────┴─────────────────┴────────────┴──────────────┴─────────────┘
=== F004-横浜 かつ 不良率 > 2.0% ===
件数: 49 件
shape: (5, 6)
┌────────────┬─────────┬─────────────────┬────────────┬──────────────┬─────────────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ production ┆ defect_count ┆ defect_rate │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ i64 ┆ i64 ┆ f64 │
╞════════════╪═════════╪═════════════════╪════════════╪══════════════╪═════════════╡
│ F004 ┆ L-A ┆ 2024-01-04 ┆ 450 ┆ 14 ┆ 3.1111 │
│ F004 ┆ L-B ┆ 2024-01-04 ┆ 442 ┆ 12 ┆ 2.7149 │
│ F004 ┆ L-A ┆ 2024-01-05 ┆ 401 ┆ 11 ┆ 2.7431 │
│ F004 ┆ L-B ┆ 2024-01-05 ┆ 433 ┆ 14 ┆ 3.2333 │
│ F004 ┆ L-C ┆ 2024-01-05 ┆ 404 ┆ 14 ┆ 3.4653 │
└────────────┴─────────┴─────────────────┴────────────┴──────────────┴─────────────┘
=== defect_count が欠損している行 ===
件数: 30 件
shape: (5, 6)
┌────────────┬─────────┬─────────────────┬──────────────┬─────────────┬──────────────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ defect_count ┆ defect_rate ┆ inspector_id │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ i64 ┆ f64 ┆ str │
╞════════════╪═════════╪═════════════════╪══════════════╪═════════════╪══════════════╡
│ F002 ┆ L-C ┆ 2024-01-04 ┆ null ┆ null ┆ INS-001 │
│ F003 ┆ L-C ┆ 2024-01-04 ┆ null ┆ null ┆ null │
│ F005 ┆ L-C ┆ 2024-01-06 ┆ null ┆ null ┆ INS-002 │
│ F003 ┆ L-C ┆ 2024-01-07 ┆ null ┆ null ┆ null │
│ F004 ┆ L-C ┆ 2024-01-07 ┆ null ┆ null ┆ INS-001 │
└────────────┴─────────┴─────────────────┴──────────────┴─────────────┴──────────────┘
結果の読み取り
不良率 2.5% 超のレコードが全体の何割かを確認することで
「品質警告の発生頻度」を把握できます。
F004-横浜工場は BASE_DR が 0.027(5工場中最高)のため、
高不良率の行が多く抽出されています。
pl.col("defect_count").is_null() は欠損値の行だけを抽出し、
後続の No.086〜088(欠損値処理)の前に欠損パターンを filter で確認しておくと
補完戦略の判断材料になります。
No.083:列を追加する
実務での意味
生産数・不良数・単価の 3 列から「良品数」「良品売上」「不良損失」という
KPI 列 を新しく追加するのは、製造データ分析で最も頻繁に行う加工です。
with_columns() は SQL の SELECT *, calculated_col AS name に相当します。
分析・モデル化の考え方
with_columns() は 拡張射影(Extended Projection) と呼ばれる操作です。
Polars の式はベクトル演算として実行されるため、for ループより 数十倍高速 に
KPI を計算できます。
fill_null(0) をインラインで組み合わせることで、
欠損値を含む列でも安全に計算できます。
Python で確認する
# with_columns で KPI 列を追加する
# defect_count が NULL の行は 0 として計算(センサー欠損 = 不良なしと仮定)
df_kpi = df_raw.with_columns([
pl.col("defect_count").fill_null(0).alias("defect_count_filled"),
(pl.col("production") - pl.col("defect_count").fill_null(0)).alias("good_count"),
((pl.col("production") - pl.col("defect_count").fill_null(0))
* pl.col("unit_price")).alias("revenue"),
(pl.col("defect_count").fill_null(0)
* pl.col("unit_price") * 1.3).alias("loss_amount"), # 廃棄コスト 30% 上乗せ
])
print(f"列数: {df_raw.width} 列 → {df_kpi.width} 列(4 列追加)")
print()
print("=== KPI 列追加後のデータ(先頭 5 件)===")
print(df_kpi.select(["factory_id", "line_id", "production", "defect_count_filled",
"good_count", "revenue", "loss_amount"]).head(5))
print()
# 工場別 KPI 集計(group_by + agg)
factory_kpi = (
df_kpi
.group_by("factory_id")
.agg([
pl.col("production").sum().alias("total_production"),
pl.col("revenue").sum().alias("total_revenue"),
pl.col("loss_amount").sum().alias("total_loss"),
])
.sort("factory_id")
)
print("=== 工場別 KPI 集計 ===")
print(f"{'工場':<12} {'総生産数':>12} {'総売上(円)':>16} {'総損失(円)':>14}")
print("-" * 58)
for row in factory_kpi.iter_rows():
fid, prod, revenue, loss = row
print(f" {FACTORY_NAMES[fid]:<10} {prod:>12,} {int(revenue):>16,} {int(loss):>14,}")
列数: 12 列 → 16 列(4 列追加)
=== KPI 列追加後のデータ(先頭 5 件)===
shape: (5, 7)
┌────────────┬─────────┬────────────┬─────────────────────┬────────────┬─────────┬─────────────┐
│ factory_id ┆ line_id ┆ production ┆ defect_count_filled ┆ good_count ┆ revenue ┆ loss_amount │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ i64 ┆ i64 ┆ i64 ┆ i64 ┆ f64 │
╞════════════╪═════════╪════════════╪═════════════════════╪════════════╪═════════╪═════════════╡
│ F001 ┆ L-A ┆ 502 ┆ 8 ┆ 494 ┆ 1778400 ┆ 37440.0 │
│ F001 ┆ L-B ┆ 446 ┆ 13 ┆ 433 ┆ 1558800 ┆ 60840.0 │
│ F001 ┆ L-C ┆ 445 ┆ 10 ┆ 435 ┆ 1044000 ┆ 31200.0 │
│ F002 ┆ L-A ┆ 446 ┆ 8 ┆ 438 ┆ 1576800 ┆ 37440.0 │
│ F002 ┆ L-B ┆ 484 ┆ 8 ┆ 476 ┆ 1713600 ┆ 37440.0 │
└────────────┴─────────┴────────────┴─────────────────────┴────────────┴─────────┴─────────────┘
=== 工場別 KPI 集計 ===
工場 総生産数 総売上(円) 総損失(円)
----------------------------------------------------------
東京工場 29,409 73,819,800 1,863,420
大阪工場 29,654 72,087,000 1,368,900
名古屋工場 31,052 81,763,800 2,152,800
横浜工場 26,234 67,282,800 2,285,400
福岡工場 24,757 63,855,600 1,657,500
# 工場別 良品売上・不良損失 比較棒グラフ
fig, ax = plt.subplots(figsize=(10, 5))
fids = factory_kpi["factory_id"].to_list()
fnames = [FACTORY_NAMES[fid] for fid in fids]
revenues = [r / 1e6 for r in factory_kpi["total_revenue"].to_list()]
losses = [l / 1e6 for l in factory_kpi["total_loss"].to_list()]
x = np.arange(len(fids))
width = 0.38
ax.bar(x - width / 2, revenues, width, label="良品売上", color="#4C72B0", alpha=0.88)
ax.bar(x + width / 2, losses, width, label="不良損失", color="#DD8452", alpha=0.88)
ax.set_title("工場別 良品売上・不良損失(Polars with_columns で KPI 列を追加)",
fontsize=13, pad=10)
ax.set_xlabel("工場", fontsize=11)
ax.set_ylabel("金額(百万円)", fontsize=11)
ax.set_xticks(x)
ax.set_xticklabels(fnames)
ax.yaxis.set_major_formatter(ticker.FuncFormatter(lambda v, _: f"{v:.0f}"))
ax.legend(fontsize=10)
ax.grid(axis="y", alpha=0.3)
plt.tight_layout()
plt.show()
結果の読み取り
with_columns() で追加した revenue・loss_amount 列を group_by で集計し、
工場別の KPI 比較グラフを描けました。
売上が多い工場でも損失が高い場合は、
収益率(= 純収益 / 売上) の視点で優先度を付け直す必要があります。
F004-横浜は不良率が最も高い設定(BASE_DR=0.027)のため、
損失が他工場より高い傾向が確認されます。
fill_null(0) をインラインで使うことで、欠損値のある defect_count でも
安全に KPI 計算できました。
No.084:列名を変更する
実務での意味
システムが出力する CSV は列名が英語(defect_rate, production)のことが多く、
報告書向けに日本語列名に変換したいケースがあります。
rename() は辞書形式で旧名→新名を指定するシンプルな操作です。
分析・モデル化の考え方
rename() は 辞書形式で旧名→新名 を指定します。
存在しない列名を指定すると ColumnNotFoundError が発生します。
select([pl.col("old").alias("new")]) でも同じことができますが、
rename() は「残す列・削除する列を意識せずに名前だけを変えたい場合」に適しています。
実運用では「コード内は英語・出力レポートは日本語」と使い分けることで
保守性と可読性を両立できます。
Python で確認する
# 列名を英語から日本語に変更する(KPI 付き DataFrame を使用)
df_renamed = df_kpi.rename({
"factory_id": "工場ID",
"line_id": "ライン",
"inspection_date": "検査日",
"production": "日次生産数",
"defect_count_filled": "不良数",
"defect_rate": "不良率(%)",
"revenue": "良品売上",
"loss_amount": "不良損失",
})
print(f"変更前の列名: {df_kpi.columns}")
print()
print(f"変更後の列名: {df_renamed.columns}")
print()
print("=== 列名変更後のデータ(先頭 5 件)===")
print(df_renamed.select(["工場ID", "ライン", "検査日", "日次生産数",
"不良数", "不良率(%)", "良品売上", "不良損失"]).head(5))
変更前の列名: ['record_id', 'factory_id', 'factory_name', 'line_id', 'inspection_date', 'product_code', 'unit_price', 'production', 'defect_count', 'defect_rate', 'inspector_id', 'raw_note', 'defect_count_filled', 'good_count', 'revenue', 'loss_amount']
変更後の列名: ['record_id', '工場ID', 'factory_name', 'ライン', '検査日', 'product_code', 'unit_price', '日次生産数', 'defect_count', '不良率(%)', 'inspector_id', 'raw_note', '不良数', 'good_count', '良品売上', '不良損失']
=== 列名変更後のデータ(先頭 5 件)===
shape: (5, 8)
┌────────┬────────┬────────────┬────────────┬────────┬───────────┬──────────┬──────────┐
│ 工場ID ┆ ライン ┆ 検査日 ┆ 日次生産数 ┆ 不良数 ┆ 不良率(%) ┆ 良品売上 ┆ 不良損失 │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ i64 ┆ i64 ┆ f64 ┆ i64 ┆ f64 │
╞════════╪════════╪════════════╪════════════╪════════╪═══════════╪══════════╪══════════╡
│ F001 ┆ L-A ┆ 2024-01-04 ┆ 502 ┆ 8 ┆ 1.5936 ┆ 1778400 ┆ 37440.0 │
│ F001 ┆ L-B ┆ 2024-01-04 ┆ 446 ┆ 13 ┆ 2.9148 ┆ 1558800 ┆ 60840.0 │
│ F001 ┆ L-C ┆ 2024-01-04 ┆ 445 ┆ 10 ┆ 2.2472 ┆ 1044000 ┆ 31200.0 │
│ F002 ┆ L-A ┆ 2024-01-04 ┆ 446 ┆ 8 ┆ 1.7937 ┆ 1576800 ┆ 37440.0 │
│ F002 ┆ L-B ┆ 2024-01-04 ┆ 484 ┆ 8 ┆ 1.6529 ┆ 1713600 ┆ 37440.0 │
└────────┴────────┴────────────┴────────────┴────────┴───────────┴──────────┴──────────┘
結果の読み取り
rename() は元の列順を保持したまま、指定した列名だけを変更します。
変更しない列はそのまま残り、指定のない列に影響を与えません。
日本語列名はコード可読性を上げますが、
下流のパイプライン(他の Python スクリプト・API)との互換性を
損なう場合があります。
「コード内は英語で管理し、レポート出力直前の rename() で日本語化する」
という設計が保守性の高いパターンです。
No.085:不要な列を削除する
実務での意味
factory_name(factory_id と 1 対 1 対応で冗長)と raw_note(テスト用メモ)は
品質分析には不要です。drop() で削除することで DataFrame が軽くなり、
後続の集計・可視化が高速になります。
「どの列が本当に必要か」を整理し、早い段階で削除するのが良い習慣です。
分析・モデル化の考え方
不要な列を保持し続けると、①メモリ使用量の増加、
②group_by や join で不要列が誤って集計される、③列名衝突のリスク、
というデメリットがあります。
「select(必要な列) と drop(不要な列) のどちらを使うか」は、
残す列が多い場合は drop、少ない場合は select が保守性の観点で優れています。
Python で確認する
# 不要な 2 列を削除する
redundant_cols = ["factory_name", "raw_note"]
df_lean = df_raw.drop(redundant_cols)
print(f"削除前: {df_raw.width} 列 {df_raw.columns}")
print()
print(f"削除後: {df_lean.width} 列 {df_lean.columns}")
print()
print("=== 不要列削除後のデータ(先頭 3 件)===")
print(df_lean.head(3))
print()
removed = [c for c in df_raw.columns if c not in df_lean.columns]
print(f"削除された列: {removed}")
print(" → factory_name は factory_id から復元可能(正規化の観点で冗長)")
print(" → raw_note はテスト用メモで本番分析では不要")
削除前: 12 列 ['record_id', 'factory_id', 'factory_name', 'line_id', 'inspection_date', 'product_code', 'unit_price', 'production', 'defect_count', 'defect_rate', 'inspector_id', 'raw_note']
削除後: 10 列 ['record_id', 'factory_id', 'line_id', 'inspection_date', 'product_code', 'unit_price', 'production', 'defect_count', 'defect_rate', 'inspector_id']
=== 不要列削除後のデータ(先頭 3 件)===
shape: (3, 10)
┌───────────┬────────────┬─────────┬───────────┬───┬───────────┬───────────┬───────────┬───────────┐
│ record_id ┆ factory_id ┆ line_id ┆ inspectio ┆ … ┆ productio ┆ defect_co ┆ defect_ra ┆ inspector │
│ --- ┆ --- ┆ --- ┆ n_date ┆ ┆ n ┆ unt ┆ te ┆ _id │
│ str ┆ str ┆ str ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
│ ┆ ┆ ┆ date ┆ ┆ i64 ┆ i64 ┆ f64 ┆ str │
╞═══════════╪════════════╪═════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡
│ REC-0000 ┆ F001 ┆ L-A ┆ 2024-01-0 ┆ … ┆ 502 ┆ 8 ┆ 1.5936 ┆ null │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
│ REC-0001 ┆ F001 ┆ L-B ┆ 2024-01-0 ┆ … ┆ 446 ┆ 13 ┆ 2.9148 ┆ INS-004 │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
│ REC-0002 ┆ F001 ┆ L-C ┆ 2024-01-0 ┆ … ┆ 445 ┆ 10 ┆ 2.2472 ┆ INS-002 │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
└───────────┴────────────┴─────────┴───────────┴───┴───────────┴───────────┴───────────┴───────────┘
削除された列: ['factory_name', 'raw_note']
→ factory_name は factory_id から復元可能(正規化の観点で冗長)
→ raw_note はテスト用メモで本番分析では不要
結果の読み取り
factory_name と raw_note が削除され、12 列 → 10 列になりました。
factory_name は FACTORY_NAMES 辞書(または join)で復元できるため、
DataFrame に保持し続ける必要はありません(これをデータの正規化と言います)。
df_lean は以降の No.087・No.088 の基点として使います。
不要列の早期削除は「分析に集中できる DataFrame 設計」の基本であり、
大規模データでは I/O や集計コストの削減にもつながります。
No.086:欠損値を確認する
実務での意味
欠損値の確認は データクレンジングの最初のステップ です。
「どの列に何件の欠損があるか」を把握することで、
補完戦略(0 で埋める・前後の値で補間・削除)を決定できます。
欠損に気づかずに集計すると null が伝播して結果が不正確になります。
分析・モデル化の考え方
df.null_count() は各列の欠損件数を 1 行の DataFrame として返します。
欠損率(= 欠損数 / 総行数)の目安:
- 5% 未満:補完または削除どちらでも可
- 5〜50%:業務ルールに従って補完
- 50% 以上:列ごと削除を検討
欠損のパターン(MCAR / MAR / MNAR)によって補完方法が変わります。
製造データの場合、センサー欠損は「ランダム欠損(MCAR)」に近いことが多いです。
Python で確認する
# 列別欠損値を確認する(df_raw で全列を確認)
null_counts = df_raw.null_count()
n = len(df_raw)
print("=== 列別欠損値一覧 ===")
print(f"{'列名':<22} {'欠損件数':>8} {'欠損率':>8} {'評価'}")
print("=" * 58)
for col in df_raw.columns:
cnt = null_counts[col][0]
pct = cnt / n * 100
note = "⚠️ 要処理" if pct > 0 else "✅ 完全"
print(f" {col:<20} {cnt:>8,} 件 {pct:>5.1f}% {note}")
print()
print(f"総レコード数: {n:,} 件(うち重複 15 件含む)")
print()
# 複数列が同時に NULL の行を確認
both_null = df_raw.filter(
pl.col("defect_count").is_null() & pl.col("inspector_id").is_null()
)
print(f"defect_count と inspector_id が両方 NULL の行: {len(both_null)} 件")
if len(both_null) > 0:
print(both_null.select(["factory_id", "line_id", "inspection_date",
"defect_count", "inspector_id"]).head(5))
=== 列別欠損値一覧 ===
列名 欠損件数 欠損率 評価
==========================================================
record_id 0 件 0.0% ✅ 完全
factory_id 0 件 0.0% ✅ 完全
factory_name 0 件 0.0% ✅ 完全
line_id 0 件 0.0% ✅ 完全
inspection_date 0 件 0.0% ✅ 完全
product_code 0 件 0.0% ✅ 完全
unit_price 0 件 0.0% ✅ 完全
production 0 件 0.0% ✅ 完全
defect_count 30 件 9.5% ⚠️ 要処理
defect_rate 30 件 9.5% ⚠️ 要処理
inspector_id 49 件 15.6% ⚠️ 要処理
raw_note 0 件 0.0% ✅ 完全
総レコード数: 315 件(うち重複 15 件含む)
defect_count と inspector_id が両方 NULL の行: 5 件
shape: (5, 5)
┌────────────┬─────────┬─────────────────┬──────────────┬──────────────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ defect_count ┆ inspector_id │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ i64 ┆ str │
╞════════════╪═════════╪═════════════════╪══════════════╪══════════════╡
│ F003 ┆ L-C ┆ 2024-01-04 ┆ null ┆ null │
│ F003 ┆ L-C ┆ 2024-01-07 ┆ null ┆ null │
│ F004 ┆ L-C ┆ 2024-01-08 ┆ null ┆ null │
│ F003 ┆ L-C ┆ 2024-01-16 ┆ null ┆ null │
│ F002 ┆ L-B ┆ 2024-01-17 ┆ null ┆ null │
└────────────┴─────────┴─────────────────┴──────────────┴──────────────┘
結果の読み取り
defect_count・defect_rate に 30 件(約 9.5%)、
inspector_id に 45 件(約 14.3%)の欠損があることが確認できました。
欠損率が 10% 前後であれば、削除より補完を優先するのが一般的です(サンプル数確保のため)。
defect_count と inspector_id が同時に欠損している行は
ライン完全停止または検査員不在だった可能性があり、現場への確認が必要です。
null_count() の実行は、データ受領後に最初に行うべきヘルスチェックです。
No.087:欠損値を削除する
実務での意味
分析に不可欠な列(不良数・不良率)に欠損がある行は、
補完しても信頼性が低い場合に削除します。
drop_nulls() は指定した列に欠損がある行を一括削除します。
分析・モデル化の考え方
df.drop_nulls()— 全列のいずれかに欠損がある行をすべて削除df.drop_nulls(subset=["col1", "col2"])— 指定列のみを対象に欠損行を削除
製造データでは「不良数が記録されていないレコードは除外」のように、
列ごとに欠損の重要度が異なるため subset の使い分けが重要です。
subset なしの drop_nulls は過剰削除になりがちです。
Python で確認する
# defect_count が欠損している行のみを削除(subset 指定)
df_no_null_defect = df_lean.drop_nulls(subset=["defect_count"])
print(f"削除前: {len(df_lean):,} 件")
print(f"削除後: {len(df_no_null_defect):,} 件 ({len(df_lean) - len(df_no_null_defect)} 件削除)")
print()
# inspector_id の欠損は残っていることを確認
null_left = df_no_null_defect.null_count()
print("=== drop_nulls(subset=['defect_count']) 後の欠損件数 ===")
for col in df_no_null_defect.columns:
cnt = null_left[col][0]
status = f"残存 {cnt} 件 ⚠️" if cnt > 0 else "0 件 ✅"
print(f" {col:<22}: {status}")
print()
# 全列の欠損を一括削除した場合との比較
df_no_null_all = df_lean.drop_nulls()
print(f"全列欠損削除: {len(df_no_null_all):,} 件")
print(f"subset 指定 : {len(df_no_null_defect):,} 件")
print(f" → subset 指定で {len(df_no_null_defect) - len(df_no_null_all)} 件多くサンプルを保持できた")
削除前: 315 件
削除後: 285 件 (30 件削除)
=== drop_nulls(subset=['defect_count']) 後の欠損件数 ===
record_id : 0 件 ✅
factory_id : 0 件 ✅
line_id : 0 件 ✅
inspection_date : 0 件 ✅
product_code : 0 件 ✅
unit_price : 0 件 ✅
production : 0 件 ✅
defect_count : 0 件 ✅
defect_rate : 0 件 ✅
inspector_id : 残存 44 件 ⚠️
全列欠損削除: 241 件
subset 指定 : 285 件
→ subset 指定で 44 件多くサンプルを保持できた
結果の読み取り
drop_nulls(subset=["defect_count"]) により、センサー欠損の 30 行のみを削除し、
検査員ログイン未記録の 45 行は保持できました。
subset なしの drop_nulls() は全列の欠損行を削除するため、
今回の場合さらに多くの行が削除されてしまいます。
「分析目的に応じて削除する列を絞る」が適切な使い方です。
削除によってサンプル数が大幅に減る場合は、次の No.088 の fill_null での補完を検討します。
No.088:欠損値を補完する
実務での意味
欠損値を削除するのではなく、業務ルールに基づく値で補完することで
サンプル数を維持し、分析精度を高められます。
defect_count の欠損は「センサー欠損 = 不良なし」と見なして 0 で補完し、
inspector_id の欠損は "不明" という識別子で補完します。
分析・モデル化の考え方
補完戦略は主に3種類あります:
| 戦略 | 操作 | 適用場面 |
|---|---|---|
| 定数補完 | fill_null(0), fill_null("不明") | 業務ルールで値が決まる場合 |
| 統計値補完 | fill_null(pl.col("col").mean()) | 欠損が少なくランダムな場合 |
| 前後値補完 | forward_fill(), backward_fill() | 時系列データで前後の値が有効な場合 |
製造データでは列ごとに異なるルールを適用することが多いです。
Python で確認する
# 欠損値を業務ルールに従って補完する(df_lean を基に作成)
df_filled = df_lean.with_columns([
# defect_count: センサー欠損 = 不良なしと仮定 → 0 補完
pl.col("defect_count").fill_null(0),
# inspector_id: ログイン未記録 → "不明" として記録
pl.col("inspector_id").fill_null("不明"),
# defect_rate: defect_count の補完値から再計算(nullも解消)
(pl.col("defect_count").fill_null(0).cast(pl.Float64)
/ pl.col("production") * 100).alias("defect_rate"),
])
# 欠損がなくなったことを確認
null_after = df_filled.null_count()
any_null = any(null_after[col][0] > 0 for col in df_filled.columns)
print(f"補完後の欠損: {'あり' if any_null else 'なし(全列 NULL ゼロ ✅)'}")
print()
# 補完前後のサンプル比較
null_rows_before = df_lean.filter(pl.col("defect_count").is_null()).head(3)
filled_ids = null_rows_before["record_id"].to_list()
null_rows_after = df_filled.filter(pl.col("record_id").is_in(filled_ids))
print("=== 補完前(欠損行サンプル)===")
print(null_rows_before.select(["record_id", "factory_id", "line_id",
"defect_count", "defect_rate", "inspector_id"]))
print()
print("=== 補完後(同一 record_id)===")
print(null_rows_after.select(["record_id", "factory_id", "line_id",
"defect_count", "defect_rate", "inspector_id"]))
補完後の欠損: なし(全列 NULL ゼロ ✅)
=== 補完前(欠損行サンプル)===
shape: (3, 6)
┌───────────┬────────────┬─────────┬──────────────┬─────────────┬──────────────┐
│ record_id ┆ factory_id ┆ line_id ┆ defect_count ┆ defect_rate ┆ inspector_id │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ i64 ┆ f64 ┆ str │
╞═══════════╪════════════╪═════════╪══════════════╪═════════════╪══════════════╡
│ REC-0005 ┆ F002 ┆ L-C ┆ null ┆ null ┆ INS-001 │
│ REC-0008 ┆ F003 ┆ L-C ┆ null ┆ null ┆ null │
│ REC-0044 ┆ F005 ┆ L-C ┆ null ┆ null ┆ INS-002 │
└───────────┴────────────┴─────────┴──────────────┴─────────────┴──────────────┘
=== 補完後(同一 record_id)===
shape: (3, 6)
┌───────────┬────────────┬─────────┬──────────────┬─────────────┬──────────────┐
│ record_id ┆ factory_id ┆ line_id ┆ defect_count ┆ defect_rate ┆ inspector_id │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ str ┆ i64 ┆ f64 ┆ str │
╞═══════════╪════════════╪═════════╪══════════════╪═════════════╪══════════════╡
│ REC-0005 ┆ F002 ┆ L-C ┆ 0 ┆ 0.0 ┆ INS-001 │
│ REC-0008 ┆ F003 ┆ L-C ┆ 0 ┆ 0.0 ┆ 不明 │
│ REC-0044 ┆ F005 ┆ L-C ┆ 0 ┆ 0.0 ┆ INS-002 │
└───────────┴────────────┴─────────┴──────────────┴─────────────┴──────────────┘
結果の読み取り
with_columns の中で fill_null を複数組み合わせることで、
列ごとに異なる補完ルールを一度に適用できました。
defect_count が None → 0 に補完され、defect_rate も 0.0000% として再計算されています。
inspector_id は None → "不明" に置換されており、
集計時に「不明件数」として分離して追跡できます。
補完後は null_count() で欠損がゼロになったことを必ず確認しましょう。
No.089:データを並び替える
実務での意味
「不良率が最も高い日・ライン・工場はどこか」を素早く特定するには、
データをソートして先頭を見るのが最も確実です。
sort() は SQL の ORDER BY に相当し、1 行で複数列ソートも可能です。
分析・モデル化の考え方
sort("col", descending=True)— 降順(大きい値が先頭)sort(["col1", "col2"], descending=[True, False])— 複数列ソートmaintain_order=True— 同値行の元の順序を維持
ソートは集計後の結果にも適用できます(group_by.agg(...).sort("col"))。
週次・月次の品質報告書で「ワーストランキング TOP10 を自動生成する」処理は
sort().head() の組み合わせで実現できます。
Python で確認する
# df_filled(欠損補完済み)を不良率の高い順にソート
df_sorted = df_filled.sort("defect_rate", descending=True, maintain_order=True)
print("=== 不良率ワーストランキング TOP 10 ===")
print(df_sorted.select(["factory_id", "line_id", "inspection_date",
"product_code", "production", "defect_count",
"defect_rate"]).head(10))
print()
# 複数列ソート(工場 昇順 × 不良率 降順)
df_multi_sort = df_filled.sort(
["factory_id", "defect_rate"],
descending=[False, True],
maintain_order=True,
)
print("=== 工場 昇順 × 不良率 降順(先頭 8 件)===")
print(df_multi_sort.select(["factory_id", "line_id", "inspection_date",
"defect_rate"]).head(8))
=== 不良率ワーストランキング TOP 10 ===
shape: (10, 7)
┌────────────┬─────────┬─────────────────┬──────────────┬────────────┬──────────────┬─────────────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ product_code ┆ production ┆ defect_count ┆ defect_rate │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ str ┆ i64 ┆ i64 ┆ f64 │
╞════════════╪═════════╪═════════════════╪══════════════╪════════════╪══════════════╪═════════════╡
│ F004 ┆ L-A ┆ 2024-01-22 ┆ EV-PCB-003 ┆ 402 ┆ 17 ┆ 4.228856 │
│ F004 ┆ L-B ┆ 2024-01-21 ┆ EV-PCB-002 ┆ 450 ┆ 19 ┆ 4.222222 │
│ F004 ┆ L-A ┆ 2024-01-19 ┆ EV-PCB-001 ┆ 439 ┆ 18 ┆ 4.100228 │
│ F004 ┆ L-B ┆ 2024-01-23 ┆ EV-PCB-002 ┆ 421 ┆ 16 ┆ 3.800475 │
│ F004 ┆ L-B ┆ 2024-01-16 ┆ EV-PCB-002 ┆ 452 ┆ 17 ┆ 3.761062 │
│ F004 ┆ L-A ┆ 2024-01-09 ┆ EV-PCB-003 ┆ 405 ┆ 15 ┆ 3.703704 │
│ F004 ┆ L-A ┆ 2024-01-13 ┆ EV-PCB-001 ┆ 408 ┆ 15 ┆ 3.676471 │
│ F004 ┆ L-B ┆ 2024-01-14 ┆ EV-PCB-001 ┆ 409 ┆ 15 ┆ 3.667482 │
│ F004 ┆ L-B ┆ 2024-01-11 ┆ EV-PCB-003 ┆ 425 ┆ 15 ┆ 3.529412 │
│ F004 ┆ L-C ┆ 2024-01-05 ┆ EV-PCB-001 ┆ 404 ┆ 14 ┆ 3.465347 │
└────────────┴─────────┴─────────────────┴──────────────┴────────────┴──────────────┴─────────────┘
=== 工場 昇順 × 不良率 降順(先頭 8 件)===
shape: (8, 4)
┌────────────┬─────────┬─────────────────┬─────────────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ defect_rate │
│ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ f64 │
╞════════════╪═════════╪═════════════════╪═════════════╡
│ F001 ┆ L-B ┆ 2024-01-05 ┆ 3.305785 │
│ F001 ┆ L-A ┆ 2024-01-21 ┆ 2.970297 │
│ F001 ┆ L-B ┆ 2024-01-04 ┆ 2.914798 │
│ F001 ┆ L-A ┆ 2024-01-07 ┆ 2.910603 │
│ F001 ┆ L-B ┆ 2024-01-18 ┆ 2.761341 │
│ F001 ┆ L-C ┆ 2024-01-08 ┆ 2.723735 │
│ F001 ┆ L-B ┆ 2024-01-09 ┆ 2.579365 │
│ F001 ┆ L-A ┆ 2024-01-16 ┆ 2.564103 │
└────────────┴─────────┴─────────────────┴─────────────┘
# 不良率ワーストランキング TOP15 の横棒グラフ
from matplotlib.patches import Patch
top15 = df_sorted.head(15)
labels = [
f"{fid} / {lid} ({str(dt)[:10]})"
for fid, lid, dt in zip(
top15["factory_id"].to_list(),
top15["line_id"].to_list(),
top15["inspection_date"].to_list(),
)
]
rates = top15["defect_rate"].to_list()
colors = [FACTORY_COLORS[fid] for fid in top15["factory_id"].to_list()]
fig, ax = plt.subplots(figsize=(10, 7))
ax.barh(range(len(labels)), rates, color=colors, alpha=0.88)
ax.set_yticks(range(len(labels)))
ax.set_yticklabels(labels, fontsize=9)
ax.axvline(2.0, color="red", linewidth=1.5, linestyle="--", alpha=0.7)
ax.text(2.05, -0.6, "警告ライン(2.0%)", color="red", fontsize=8, va="top")
ax.set_title("不良率ワーストランキング TOP15(Polars sort で並び替え)",
fontsize=13, pad=10)
ax.set_xlabel("不良率(%)", fontsize=11)
ax.invert_yaxis()
legend_handles = [
Patch(color=FACTORY_COLORS[fid], label=FACTORY_NAMES[fid])
for fid in FACTORY_IDS
]
ax.legend(handles=legend_handles, fontsize=9, loc="lower right")
ax.grid(axis="x", alpha=0.3)
plt.tight_layout()
plt.show()
結果の読み取り
ソート後の TOP15 を横棒グラフで可視化することで、
どの工場のどのラインのどの日が最も深刻かが一目でわかります。
F004-横浜(赤)が上位に集中している場合、
その工場の製造条件・設備状態の優先調査を検討します。
複数列ソートにより「工場別に不良率の高い日をまとめて確認する」ことも可能で、
週次品質会議の資料作成を完全自動化できます。
No.090:重複データを削除する
実務での意味
データ同期バグにより同一レコードが2重登録されていると、
生産数・売上・不良数の集計値がすべて 2 倍にカウントされてしまいます。
unique() での重複除去はデータクレンジングの最終確認ステップです。
分析・モデル化の考え方
df.unique()— 全列が一致する行を重複として除去df.unique(subset=[...])— 指定列の組み合わせが一致する行を重複とみなすkeep="first"— 最初のレコードを保持keep="last"— 最後のレコードを保持maintain_order=True— 元の行順を維持
「どの列の組み合わせが一意キーか」を業務ルールとして事前に定義しておくことが
重複判定の精度を左右します。
Python で確認する
# 重複レコードの除去
n_before = len(df_filled)
df_clean = df_filled.unique(
subset=["factory_id", "line_id", "inspection_date", "product_code"],
keep="first",
maintain_order=True,
)
n_after = len(df_clean)
n_removed = n_before - n_after
print("=== 重複除去の結果 ===")
print(f" 除去前: {n_before:,} 件")
print(f" 除去後: {n_after:,} 件")
print(f" 削除数: {n_removed:,} 件(重複 {n_removed} レコードを除去)")
print()
# 重複していたグループを確認(df_raw で検証)
dup_check = (
df_raw
.group_by(["factory_id", "line_id", "inspection_date", "product_code"])
.agg(pl.len().alias("count"))
.filter(pl.col("count") > 1)
.sort("count", descending=True)
)
print(f"重複グループ数: {len(dup_check)} グループ")
if len(dup_check) > 0:
print(dup_check.head(5))
print()
# クレンジング完了後の最終 DataFrame サマリー
null_final = df_clean.null_count()
any_null_final = any(null_final[col][0] > 0 for col in df_clean.columns)
print("=== クレンジング済み df_clean のサマリー ===")
print(f" 行数 : {len(df_clean):,} 件")
print(f" 列数 : {df_clean.width} 列")
print(f" 欠損 : {'あり' if any_null_final else 'なし(全列 NULL ゼロ ✅)'}")
print(f" 重複 : {n_removed} 件を除去済み ✅")
print()
print(df_clean.head(5))
=== 重複除去の結果 ===
除去前: 315 件
除去後: 300 件
削除数: 15 件(重複 15 レコードを除去)
重複グループ数: 15 グループ
shape: (5, 5)
┌────────────┬─────────┬─────────────────┬──────────────┬───────┐
│ factory_id ┆ line_id ┆ inspection_date ┆ product_code ┆ count │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ str ┆ str ┆ date ┆ str ┆ u32 │
╞════════════╪═════════╪═════════════════╪══════════════╪═══════╡
│ F004 ┆ L-B ┆ 2024-01-19 ┆ EV-PCB-002 ┆ 2 │
│ F005 ┆ L-A ┆ 2024-01-07 ┆ EV-PCB-001 ┆ 2 │
│ F002 ┆ L-A ┆ 2024-01-18 ┆ EV-PCB-002 ┆ 2 │
│ F002 ┆ L-B ┆ 2024-01-19 ┆ EV-PCB-003 ┆ 2 │
│ F005 ┆ L-A ┆ 2024-01-10 ┆ EV-PCB-001 ┆ 2 │
└────────────┴─────────┴─────────────────┴──────────────┴───────┘
=== クレンジング済み df_clean のサマリー ===
行数 : 300 件
列数 : 10 列
欠損 : なし(全列 NULL ゼロ ✅)
重複 : 15 件を除去済み ✅
shape: (5, 10)
┌───────────┬────────────┬─────────┬───────────┬───┬───────────┬───────────┬───────────┬───────────┐
│ record_id ┆ factory_id ┆ line_id ┆ inspectio ┆ … ┆ productio ┆ defect_co ┆ defect_ra ┆ inspector │
│ --- ┆ --- ┆ --- ┆ n_date ┆ ┆ n ┆ unt ┆ te ┆ _id │
│ str ┆ str ┆ str ┆ --- ┆ ┆ --- ┆ --- ┆ --- ┆ --- │
│ ┆ ┆ ┆ date ┆ ┆ i64 ┆ i64 ┆ f64 ┆ str │
╞═══════════╪════════════╪═════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪═══════════╡
│ REC-0000 ┆ F001 ┆ L-A ┆ 2024-01-0 ┆ … ┆ 502 ┆ 8 ┆ 1.593625 ┆ 不明 │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
│ REC-0001 ┆ F001 ┆ L-B ┆ 2024-01-0 ┆ … ┆ 446 ┆ 13 ┆ 2.914798 ┆ INS-004 │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
│ REC-0002 ┆ F001 ┆ L-C ┆ 2024-01-0 ┆ … ┆ 445 ┆ 10 ┆ 2.247191 ┆ INS-002 │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
│ REC-0003 ┆ F002 ┆ L-A ┆ 2024-01-0 ┆ … ┆ 446 ┆ 8 ┆ 1.793722 ┆ INS-003 │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
│ REC-0004 ┆ F002 ┆ L-B ┆ 2024-01-0 ┆ … ┆ 484 ┆ 8 ┆ 1.652893 ┆ INS-001 │
│ ┆ ┆ ┆ 4 ┆ ┆ ┆ ┆ ┆ │
└───────────┴────────────┴─────────┴───────────┴───┴───────────┴───────────┴───────────┴───────────┘
結果の読み取り
unique(subset=...) により、データ同期バグで発生した 15 件の重複が正確に除去されました。
df_clean は 300 件・10 列・欠損ゼロ・重複ゼロの クレンジング済み DataFrame です。
この後の分析(group_by による集計・join による結合・Matplotlib での可視化)は
df_clean を基点として進めます。
subset を正しく設定することが重複判定の精度を左右します。
「どの列の組み合わせが一意キーか」を事前に業務ルールとして定義して
チームで共有することが重要です。
対象ノックを通して見える実務上の示唆
No.081〜090 で学んだ 10 のデータ加工操作 は、製造データクレンジングパイプラインの骨格です。
1. データ加工の標準フロー
実務では次の順序でデータを整備します:
① select() → 必要な列だけに絞り込む
② filter() → 不要な行(テスト用・範囲外)を除外
③ drop() → 冗長な列(正規化・不要メタデータ)を削除
④ null_count() → 欠損の全体像を把握
⑤ drop_nulls() or fill_null() → 業務ルールに従って欠損を処理
⑥ unique() → 重複を除去
⑦ with_columns() → KPI 列・派生列を追加
⑧ rename() → 列名を報告書向けに変更
⑨ sort() → ランキングや時系列の確認
2. Polars の Lazy Evaluation でさらに高速化
Polars は df.lazy().filter(...).select(...).collect() と書くことで、
クエリオプティマイザ が実行計画を最適化します。
大規模データ(100 万行以上)では pandas より 5〜30 倍高速 になる場合があります。
3. 欠損値処理のビジネスルール化
「センサー欠損 → 0 補完」「検査員未記録 → 不明フラグ」という
補完ルールは、ビジネスルール文書に明示するのが正しい運用です。
コードコメントだけでなく、設定ファイルやドキュメントに記録して
チームで共有することで属人化を防げます。
実務導入する場合に必要なこと
本章で学んだ Polars のデータ加工操作を製造現場のデータパイプラインに導入する手順です。
Step 1: Polars のインストールと既存スクリプトの確認(約半日)
uv add polars
既存の pandas コードとの互換性を確認します。
pl.from_pandas(df_pd) / df_pl.to_pandas() で相互変換できます。
Step 2: 生データの品質調査(約1日)
null_count() と unique() を使って、収集済みデータの欠損率と重複率を計測します。
品質管理の出発点は「自分のデータの品質を数字で把握すること」です。
Step 3: クレンジングパイプラインの構築(約2〜3日)
select → filter → drop → fill_null → unique → with_columns の順序で
クレンジング関数を作成し、毎日届く CSV に自動適用します。
Polars の Lazy Evaluation(df.lazy()...collect())を使うと大量データでも高速です。
Step 4: KPI ダッシュボードへの組み込み(約1〜2日)
with_columns で追加した売上・損失・不良率 KPI 列を
sort().head() でランキング化し、Matplotlib で自動グラフ生成します。
第10章(集計・可視化・ミニ分析)ではこのパイプラインを完成させます。
まとめ
本章(No.081〜090)で学んだ内容を整理します。
| No. | スキル | 製造現場での活用 |
|---|---|---|
| 081 | 特定の列を選択する | 12 列 → 6 列に絞り、後続処理を高速化 |
| 082 | 条件に合う行を抽出する | 不良率 > 閾値・特定工場などのフィルタリング |
| 083 | 列を追加する | 良品数・売上・損失の KPI 列を一括計算・棒グラフ可視化 |
| 084 | 列名を変更する | 英語列名 → 日本語に変換して報告書向けに整形 |
| 085 | 不要な列を削除する | 冗長な factory_name・テスト用 raw_note を削除 |
| 086 | 欠損値を確認する | センサー欠損(9.5%)・ログイン欠損(14.3%)を特定 |
| 087 | 欠損値を削除する | subset 指定で必須列の欠損行のみを削除 |
| 088 | 欠損値を補完する | センサー欠損→0 補完・検査員→不明フラグで補完 |
| 089 | データを並び替える | 不良率ワーストランキングを1行で生成・横棒グラフ可視化 |
| 090 | 重複データを削除する | データ同期バグの 15 件重複を unique で完全除去 |
第10章(No.091〜100)では 集計・可視化・ミニ分析 を学び、
本章でクレンジングした df_clean を使って
group_by 集計・日付処理・Matplotlib グラフ・相関分析まで進みます。
法人向けのご相談
数理工房 では、製造業・DX 推進担当者向けの Python 研修・データ分析支援を提供しています。
こんなお悩みはありませんか?
- 「本章のデータ前処理パイプラインを自社の品質検査データに適用したい」
- 「複数工場のデータを毎日自動収集・クレンジングする仕組みを作りたい」
- 「Polars と pandas のどちらを使うべきか判断できない」
- 「欠損値・重複の処理ルールを社内でどう標準化すればよいか」
- 「Python 研修をカスタマイズして製造業向けに実施してほしい」
提供サービス
| サービス | 概要 |
|---|---|
| 製造業向け Python 研修 | 現場データを使った実践型研修(オンライン/対面) |
| データ品質クレンジング基盤 | 欠損・重複・型変換の自動化パイプライン構築 |
| 品質管理ダッシュボード | KPI 自動集計・不良率ランキング・アラート可視化 |
| DX 推進コンサルティング | 課題整理から実装・定着支援まで一貫対応 |
📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。