🐍

(学習ログ)Python017:pandasによる売上・顧客データのクレンジングと欠損値補完

に公開

1. はじめに

この記事の目的

実務でよく遭遇する「データが汚れている」状況に対処するため、pandasを使った基本的なデータクレンジング手法を身につけることを目指します。

この記事の概要

本記事では、売上データと顧客台帳という2つの異なるファイルを読み込み、以下の処理を実践的に学びます:

  • CSVとExcelファイルの読み込み
  • 日付データの変換と整形
  • 文字列データのクレンジング(全角・半角スペース削除、大文字統一)
  • 欠損値の検出と補完ロジック
  • Excelシリアル値形式の日付変換

これらは実務のデータ分析で最初に行う重要な前処理ステップです。

2. サンプルコード

コード全文

import pandas as pd

# ファイル読み込み
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(" ", "")  # 半角スペース削除

# 売上データ:欠損値の補完処理
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(" ", "")  # 半角スペース削除

# 顧客台帳:Excelシリアル値と文字列が混在する日付の変換
flg_is_serial = sample_customer_ledger["登録日"].astype("str").str.isdigit()  # 数値(シリアル値)判定
# シリアル値からdatetime型へ変換
fromSerial = pd.to_timedelta(sample_customer_ledger.loc[flg_is_serial, "登録日"].astype("float") - 2, unit="D") + pd.to_datetime('1900/1/1')
# 文字列からdatetime型へ変換
fromString = pd.to_datetime(sample_customer_ledger.loc[~flg_is_serial, "登録日"])
# 2つの結果を結合
sample_customer_ledger["登録日"] = pd.concat([fromSerial, fromString])

# 登録年月列の作成
sample_customer_ledger["登録年月"] = sample_customer_ledger["登録日"].dt.strftime("%Y%m")

# 年月ごとの顧客数を集計
rslt = sample_customer_ledger.groupby("登録年月").count()["顧客名"]
print(rslt)
print(len(sample_customer_ledger))

# 確認:シリアル値形式のデータ数
flg_is_serial = sample_customer_ledger["登録日"].astype("str").str.isdigit()
flg_is_serial.sum()

コード解説

ファイル読み込み部分

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

CSVファイルとExcelファイルをそれぞれ読み込みます。pandasはファイル形式に応じた専用関数を提供しています。

日付データの変換

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")

文字列形式の日付をdatetime型に変換し、年月だけを抽出して新しい列を作成します。dt.strftime()で任意のフォーマットに整形できます。

文字列のクレンジング

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(" ", "")

商品名を統一するため、全て大文字化し、全角・半角スペースを削除します。これにより「りんご」「リンゴ 」「RINGO」などの表記ゆれを防ぎます。

欠損値の補完処理

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

価格が欠損している行を検出し、同じ商品名の中で最大価格を取得して補完します。この手法は「同一商品は基本的に同じ価格である」という前提に基づいています。

Excelシリアル値の変換

flg_is_serial = sample_customer_ledger["登録日"].astype("str").str.isdigit()
fromSerial = pd.to_timedelta(sample_customer_ledger.loc[flg_is_serial, "登録日"].astype("float") - 2, unit="D") + pd.to_datetime('1900/1/1')
fromString = pd.to_datetime(sample_customer_ledger.loc[~flg_is_serial, "登録日"])
sample_customer_ledger["登録日"] = pd.concat([fromSerial, fromString])

Excelの日付セルが数値(シリアル値)で保存されている場合と、文字列で保存されている場合の両方に対応します。シリアル値は1900年1月1日からの経過日数なので、適切に変換する必要があります。

集計処理

rslt = sample_customer_ledger.groupby("登録年月").count()["顧客名"]
print(rslt)

登録年月ごとに顧客数をカウントし、月次の登録状況を把握します。

実行結果例

登録年月
202301    15
202302    23
202303    18
202304    21
202305    19
Name: 顧客名, dtype: int64
96

この出力は以下を示しています:

  • 2023年1月から5月まで、各月の新規顧客登録数
  • 顧客台帳の総件数は96件

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

代表的なエラー文

KeyError: 'column_name'

原因:指定した列名がDataFrameに存在しない
対処:columns属性で列名一覧を確認し、正確な列名を使用する

ValueError: time data '...' does not match format

原因:日付データのフォーマットが想定と異なる
対処:pd.to_datetime()のformat引数を指定するか、errors='coerce'で無効な値をNaTに変換

TypeError: unsupported operand type(s)

原因:数値演算を行う列に文字列やNaNが含まれている
対処:astype()で型変換するか、dropna()で欠損値を除外

AttributeError: 'Series' object has no attribute 'str'

原因:数値型の列に対して.strアクセサを使用している
対処:対象列が文字列型(object型)であることを確認する

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

データ構造の確認

sample_uriage_data.shape
# 出力例: (500, 5)  → 500行、5列のデータ

列名の確認

sample_uriage_data.columns
# 出力例: Index(['purchase_date', 'customer_id', 'item_name', 'item_price', 'quantity'], dtype='object')

先頭データの確認

sample_uriage_data.head(3)
# 出力例:
#   purchase_date  customer_id  item_name  item_price  quantity
# 0    2023-01-15         C001       APPLE         150         2
# 1    2023-01-16         C002      BANANA         100         5
# 2    2023-01-16         C001      ORANGE         200         1

データ型の確認

sample_uriage_data.dtypes
# 出力例:
# purchase_date     datetime64[ns]
# customer_id               object
# item_name                 object
# item_price               float64
# quantity                   int64
# dtype: object

欠損値の確認

sample_uriage_data.isnull().sum()
# 出力例:
# purchase_date    0
# customer_id      0
# item_name        0
# item_price       8
# quantity         0
# dtype: int64

ユニーク値の確認

sample_uriage_data["item_name"].unique()
# 出力例: array(['APPLE', 'BANANA', 'ORANGE', 'GRAPE', 'MELON'], dtype=object)

4. まとめ

本記事を通じて、以下のデータクレンジングスキルが身につきます。

  • 複数形式のファイル読み込みと日付変換:CSVとExcelファイルを適切に読み込み、文字列形式の日付をdatetime型に変換して年月データを作成できるようになりました

  • 実務的な文字列クレンジング:大文字統一、全角・半角スペース削除により、表記ゆれによるデータの不一致を防ぎ、正確な集計やマージができるようになりました

  • 欠損値の検出と論理的な補完:単純に削除するのではなく、同一商品の最大価格で補完するなど、ビジネスロジックに基づいた欠損値処理の考え方を習得しました

Discussion