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 のデータ加工は直感的に見えますが、製造データの文脈では次の注意点があります。

  1. select vs drop の使い分け
    「残したい列が少ない場合は select」「削除したい列が少ない場合は drop」が基本方針です。
    列数が多い実務データでは drop の方が保守性が高いことが多いです

  2. filter の論理演算
    Polars では &(AND)と |(OR)を使います。Python の and / or は使えません。
    複数条件は必ず丸括弧 () で囲む必要があります
    例:df.filter((pl.col("a") > 2) & (pl.col("b") == "X"))

  3. with_columns の同時評価
    with_columns 内の複数の式は 元の DataFrame を基準に同時評価 されます。
    「列 A の計算結果を列 B の式で使いたい」場合は with_columns を2回呼ぶか
    チェーン(.with_columns(...).with_columns(...))を使います

  4. 欠損値処理の順序が重要
    「欠損値を何で埋めるか」を業務ルールとして決めてから操作します。
    「0 で埋める」「前後の値で埋める」「削除する」は
    分析の前提条件に直結するため、コメントで明示してください

  5. uniquekeep 戦略
    重複除去の際、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 のメモリ使用量を減らし、
後続の filtergroup_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()

svg

結果の読み取り

with_columns() で追加した revenueloss_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_byjoin で不要列が誤って集計される、③列名衝突のリスク、
というデメリットがあります。
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_nameraw_note が削除され、12 列 → 10 列になりました。
factory_nameFACTORY_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_countdefect_rate に 30 件(約 9.5%)、
inspector_id に 45 件(約 14.3%)の欠損があることが確認できました。
欠損率が 10% 前後であれば、削除より補完を優先するのが一般的です(サンプル数確保のため)。
defect_countinspector_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_countNone → 0 に補完され、defect_rate0.0000% として再計算されています。
inspector_idNone → "不明" に置換されており、
集計時に「不明件数」として分離して追跡できます。
補完後は 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()

svg

結果の読み取り

ソート後の 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
まずはお気軽にご相談ください。