🐍

(学習ログ)Python016:販売データのクレンジングと欠損値補完

に公開

1. はじめに

この記事の目的

実務でよく遭遇する「汚れたデータ」を、pandasを使って効率的にクリーニングする方法を学びます。

この記事の概要

本記事では、販売データ(CSV)と顧客台帳(Excel)を読み込み、以下の処理を行います:

  • 日付データの変換と月次集計用の加工
  • 商品名の表記ゆれを統一(大文字化・空白除去)
  • 欠損している価格データを、同じ商品の既存データから補完
  • データの並び替えと欠損値の最終確認

2. サンプルコード

コード全文

import pandas as pd

# CSVとExcelファイルの読み込み
sample_uriage_data = pd.read_csv("002_サンプル/sample_uriage_data.csv")
sample_customer_ledger = pd.read_excel("002_サンプル/sample_customer_ledger.xlsx")

# 日付型への変換と購入月の抽出
sample_uriage_data["purchase_date"] = pd.to_datetime(sample_uriage_data["purchase_date"])
sample_uriage_data["purchase_month"] = sample_uriage_data["purchase_date"].dt.strftime("%Y%m")

# 商品名の正規化:大文字化
sample_uriage_data["item_name"] = sample_uriage_data["item_name"].str.upper()
# 商品名の正規化:全角スペース削除
sample_uriage_data["item_name"] = sample_uriage_data["item_name"].str.replace(" ", "")
# 商品名の正規化:半角スペース削除
sample_uriage_data["item_name"] = sample_uriage_data["item_name"].str.replace(" ", "")

# 欠損値の補完処理
# 価格がNaNの行を特定するフラグを作成
flg_is_null = sample_uriage_data["item_price"].isnull()
# 価格が欠損している商品名ごとにループ処理
for NaN_item_name in list(sample_uriage_data.loc[flg_is_null, "item_name"].unique()):
    # 同じ商品名で価格が存在する行から最大値を取得
    impute_price = sample_uriage_data.loc[(~flg_is_null) & (sample_uriage_data["item_name"]==NaN_item_name), "item_price"].max()
    # 欠損値を補完
    sample_uriage_data.loc[(flg_is_null) & (sample_uriage_data["item_name"]==NaN_item_name), "item_price"] = impute_price

# 商品名でソート(昇順)
sample_uriage_data.sort_values(by=["item_name"], ascending=True)
# 各列に欠損値が残っていないか確認
sample_uriage_data.isnull().any(axis=0)

# 顧客台帳の顧客名も同様に正規化
sample_customer_ledger["顧客名"] = sample_customer_ledger["顧客名"].str.replace(" ", "")
sample_customer_ledger["顧客名"] = sample_customer_ledger["顧客名"].str.replace(" ", "")
# 結果の確認
sample_customer_ledger.head()

コード解説

データの読み込み部分

sample_uriage_data = pd.read_csv("002_サンプル/sample_uriage_data.csv")
sample_customer_ledger = pd.read_excel("002_サンプル/sample_customer_ledger.xlsx")

CSVファイルはread_csv()、Excelファイルはread_excel()で読み込みます。それぞれDataFrame型のオブジェクトとして格納されます。

日付データの加工

sample_uriage_data["purchase_date"] = pd.to_datetime(sample_uriage_data["purchase_date"])
sample_uriage_data["purchase_month"] = sample_uriage_data["purchase_date"].dt.strftime("%Y%m")

to_datetime()で文字列を日付型に変換し、dt.strftime("%Y%m")で「202501」のような年月形式の文字列を生成します。これにより月次集計が容易になります。

商品名の正規化

sample_uriage_data["item_name"] = sample_uriage_data["item_name"].str.upper()
sample_uriage_data["item_name"] = sample_uriage_data["item_name"].str.replace(" ", "")
sample_uriage_data["item_name"] = sample_uriage_data["item_name"].str.replace(" ", "")

str.upper()で全て大文字に統一し、str.replace()で全角・半角スペースを削除します。これにより「Apple Pie」「APPLE PIE」「apple pie」などの表記ゆれを統一できます。

欠損値の補完ロジック

flg_is_null = sample_uriage_data["item_price"].isnull()
for NaN_item_name in list(sample_uriage_data.loc[flg_is_null, "item_name"].unique()):
    impute_price = sample_uriage_data.loc[(~flg_is_null) & (sample_uriage_data["item_name"]==NaN_item_name), "item_price"].max()
    sample_uriage_data.loc[(flg_is_null) & (sample_uriage_data["item_name"]==NaN_item_name), "item_price"] = impute_price

価格が欠損している商品を特定し、同じ商品名で価格が記録されている行から最大値を取得して補完します。これにより、「商品Aの価格が一部の行で空欄」という状況に対応できます。

データの確認

sample_uriage_data.sort_values(by=["item_name"], ascending=True)
sample_uriage_data.isnull().any(axis=0)

商品名でソートして見やすくし、isnull().any(axis=0)で各列に欠損値が残っていないかを確認します。

実行結果例

元データ(sample_uriage_data.csv)

purchase_date,item_name,item_price
2025-01-15,Apple Pie,500
2025-01-16,APPLE PIE,
2025-01-17,apple pie,520
2025-01-18,Banana Cake,300

処理後のpurchase_month列

202501
202501
202501
202501

正規化後のitem_name列

APPLEPIE
APPLEPIE
APPLEPIE
BANANACAKE

欠損値補完後のitem_price列

500
520  ← 最大値520で補完された
520
300

欠損値チェックの結果

sample_uriage_data.isnull().any(axis=0)
purchase_date    False
item_name        False
item_price       False
purchase_month   False
dtype: bool

全ての列でFalseとなり、欠損値が存在しないことが確認できます。

3. エラーと確認のポイント

代表的なエラー文

エラー1: FileNotFoundError

FileNotFoundError: [Errno 2] No such file or directory: '002_サンプル/sample_uriage_data.csv'

原因: ファイルパスが間違っているか、ファイルが存在しません。カレントディレクトリとファイルの配置を確認しましょう。

エラー2: KeyError

KeyError: 'item_price'

原因: 指定した列名がDataFrameに存在しません。df.columnsで列名を確認し、スペルミスや全角・半角の違いをチェックしましょう。

エラー3: AttributeError

AttributeError: Can only use .str accessor with string values

原因: 文字列型でない列に.strメソッドを使っています。df.dtypesで型を確認し、必要に応じてastype(str)で変換しましょう。

よく使う確認メソッドと出力例

shape - データの行数と列数を確認

sample_uriage_data.shape
(150, 4)  # 150行、4列のデータ

columns - 列名の一覧を確認

sample_uriage_data.columns
Index(['purchase_date', 'item_name', 'item_price', 'purchase_month'], dtype='object')

head() - 先頭5行を確認

sample_uriage_data.head()
  purchase_date     item_name  item_price purchase_month
0    2025-01-15      APPLEPIE         500         202501
1    2025-01-16      APPLEPIE         520         202501
2    2025-01-17      APPLEPIE         520         202501
3    2025-01-18   BANANACAKE         300         202501
4    2025-01-19  CHEESECAKE           450         202501

dtypes - 各列のデータ型を確認

sample_uriage_data.dtypes
purchase_date     datetime64[ns]
item_name                 object
item_price               float64
purchase_month            object
dtype: object

isnull().sum() - 各列の欠損値の数を確認

sample_uriage_data.isnull().sum()
purchase_date     0
item_name         0
item_price        0
purchase_month    0
dtype: int64

4. まとめ

本記事では、実務で頻繁に発生するデータクレンジングの基本的な流れを学びました。

  • 日付データの加工と文字列の正規化: to_datetime()str.upper()str.replace()を使うことで、表記ゆれを統一し、後続の集計処理をスムーズにできる
  • 欠損値の賢い補完方法: 単純に平均値で埋めるのではなく、同じ商品名のデータから最大値(または最頻値)を取得することで、より実態に即した補完が可能
  • 処理後の確認を怠らない: isnull().any()head()dtypesなどで、意図通りに処理が完了しているかを必ず確認する習慣をつけることが、バグの早期発見につながる

Discussion