🐍

(学習ログ)Python018:販売データと顧客台帳のクレンジング・結合処理

に公開

1. はじめに

この記事の目的

pandasを使った実務的なデータクレンジングと結合の基本パターンを習得することを目指します。

この記事の概要

本記事では、販売データ(CSV)と顧客台帳(Excel)を読み込み、文字列の正規化、欠損値の補完、Excel特有のシリアル値形式の日付変換を経て、最終的に2つのデータを結合するまでの一連の流れを解説します。

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

# 顧客台帳:登録日がシリアル値(数値)か文字列かを判定
flg_is_serial = sample_customer_ledger["登録日"].astype("str").str.isdigit()
# シリアル値をExcel基準日(1900/1/1)からの経過日数として日付に変換
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])
sample_customer_ledger["登録年月"] = sample_customer_ledger["登録日"].dt.strftime("%Y%m")

# 売上データと顧客台帳を結合
sample_join_data = pd.merge(sample_uriage_data, sample_customer_ledger, left_on="customer_name", right_on="顧客名", how="left")
# 重複列(customer_name)を削除
sample_join_data = sample_join_data.drop("customer_name", axis=1)
sample_join_data

コード解説

データの読み込み

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

文字列として読み込まれた購入日を日付型に変換し、年月(例:202401)を抽出して新しい列を作成します。

商品名の正規化

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

商品価格が欠損している行について、同じ商品名を持つ他の行の最高価格を使って補完します。これにより、同一商品の価格は統一されます。

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から読み込んだ日付がシリアル値(数値)の場合と文字列の場合が混在していることがあります。それぞれを正しい日付型に変換し、統合します。

データの結合

sample_join_data = pd.merge(sample_uriage_data, sample_customer_ledger, left_on="customer_name", right_on="顧客名", how="left")
sample_join_data = sample_join_data.drop("customer_name", axis=1)

売上データに顧客台帳の情報を紐付けます。left_onright_onで異なる列名を指定できます。左外部結合(how="left")により、売上データの全ての行を保持します。

実行結果例

元データの想定(sample_uriage_data)

   customer_name    item_name  item_price purchase_date purchase_month
0        山田太郎     apple          NaN    2024-01-15         202401
1        佐藤花子    APPLE         150.0    2024-01-20         202401
2        鈴木一郎     Banana        200.0    2024-02-10         202402

クレンジング後

   customer_name item_name  item_price purchase_date purchase_month
0        山田太郎     APPLE        150.0    2024-01-15         202401
1        佐藤花子     APPLE        150.0    2024-01-20         202401
2        鈴木一郎    BANANA        200.0    2024-02-10         202402

顧客台帳(sample_customer_ledger)

   顧客名      登録日    登録年月
0  山田太郎  2023-12-01   202312
1  佐藤花子  2024-01-05   202401

結合後(sample_join_data)

   顧客名  item_name  item_price purchase_date purchase_month    登録日    登録年月
0  山田太郎    APPLE        150.0    2024-01-15         202401 2023-12-01   202312
1  佐藤花子    APPLE        150.0    2024-01-20         202401 2024-01-05   202401
2  鈴木一郎   BANANA        200.0    2024-02-10         202402        NaN      NaN

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

代表的なエラー文

KeyError: 'column_name'

KeyError: 'purchase_date'

指定した列名がDataFrameに存在しない場合に発生します。列名のスペルミスや、読み込み時の文字コード問題を確認しましょう。

ValueError: unconverted data remains

ValueError: unconverted data remains: -01-15

日付の形式が想定と異なる場合に発生します。pd.to_datetime()formatパラメータで明示的に指定するか、errors='coerce'で不正な値をNaTに変換できます。

TypeError: cannot concatenate object of type

TypeError: can only concatenate str (not "int") to str

データ型が混在している場合に発生します。.astype()で型を揃えてから処理しましょう。

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

shape(データの行数・列数確認)

sample_uriage_data.shape
# 出力例: (1000, 5)  # 1000行、5列

columns(列名の確認)

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

head(先頭データの確認)

sample_uriage_data.head(3)
# 出力例: 先頭3行が表形式で表示される

isnull().sum()(欠損値の個数確認)

sample_uriage_data.isnull().sum()
# 出力例:
# customer_name     0
# item_name         0
# item_price       15
# purchase_date     0
# purchase_month    0
# dtype: int64

dtypes(データ型の確認)

sample_uriage_data.dtypes
# 出力例:
# customer_name            object
# item_name                object
# item_price              float64
# purchase_date    datetime64[ns]
# purchase_month           object
# dtype: object

value_counts()(値の分布確認)

sample_uriage_data["item_name"].value_counts()
# 出力例:
# APPLE     450
# BANANA    320
# ORANGE    230
# Name: item_name, dtype: int64

4. まとめ

本記事を通じて、以下の実践的なデータ処理技術を習得できました。

  • 異なる形式のデータの統合処理:CSVとExcelを読み込み、文字列の正規化や日付変換を行うことで、分析可能な状態に整える手法
  • 実務的な欠損値補完のロジック:同一商品の最高価格で補完するなど、ビジネスルールに基づいた欠損値処理の実装方法
  • Excelのシリアル値対応:Excelから読み込んだ日付データの形式混在に対応し、正しい日付型に統一する技術

Discussion