🐍

(学習ログ)Python019:売上データと顧客台帳の前処理とマージ

に公開

1. はじめに

この記事の目的

PandasライブラリによるCSV・Excelファイルの読み込みから、データクリーニング、欠損値補完、そして複数データの結合まで、実務でよく使われる一連のデータ前処理フローを理解することを目指します。

この記事の概要

本記事では、売上データと顧客台帳という2つのデータソースを扱います。
日付の型変換、文字列の正規化、欠損値の補完といった基本的なクリーニング処理を行った後、両者をマージして分析可能な形式で出力するまでの流れを解説します。

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(" ", "")   # 半角空白削除

# 欠損値がある商品価格を、同じ商品名の最大価格で補完
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()
# シリアル値から日付への変換
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")
# 重複する顧客名列を削除
sample_join_data = sample_join_data.drop("customer_name", axis=1)

# 必要な列だけを抽出してCSVに出力
dump_data = sample_join_data[["purchase_date", "purchase_month", "item_name", "item_price", "顧客名", "かな", "地域", "メールアドレス", "登録日"]]
dump_data.to_csv("dump_data.csv", index=False)

コード解説

データ読み込み部分(1〜3行目)

  • pd.read_csv() でCSVファイルを、pd.read_excel() でExcelファイルをそれぞれDataFrame形式で読み込みます
  • 実務では異なる形式のファイルを扱うことが多いため、両方の読み込み方法に慣れておくことが重要です

日付の処理(6〜7行目)

  • pd.to_datetime() で文字列形式の日付を日付型に変換します
  • dt.strftime("%Y%m") で「202401」のような年月形式の文字列を作成し、月次集計に備えます

文字列の正規化(10〜12行目)

  • str.upper() で商品名を大文字に統一することで、表記ゆれを防ぎます
  • str.replace() で全角・半角の空白を削除し、データの一貫性を確保します

欠損値の補完(15〜20行目)

  • isnull() で欠損値のフラグを作成します
  • 同じ商品名で価格が存在するレコードから最大値を取得し、欠損値に代入します
  • この方法により、商品ごとに妥当な価格を自動補完できます

データ確認(23〜24行目)

  • sort_values() でデータをソートして確認しやすくします
  • isnull().any(axis=0) で各列に欠損値が残っていないかチェックします

顧客名の正規化(27〜28行目)

  • 売上データと同様に、顧客名からも空白を削除してマージ時の不一致を防ぎます

Excelシリアル値の処理(31〜36行目)

  • Excelで日付がシリアル値(数値)として保存されている場合に対応します
  • str.isdigit() で数値かどうかを判定し、数値の場合は日付に変換します
  • 文字列の日付と数値の日付を pd.concat() で結合して、統一した日付列を作成します

データのマージ(39〜41行目)

  • pd.merge() で売上データと顧客台帳を結合します
  • left_onright_on で異なる列名の結合キーを指定します
  • how="left" により、売上データを基準とした左結合を行います

出力処理(44〜45行目)

  • 必要な列だけを選択してから、to_csv() でCSVファイルに出力します
  • index=False でインデックス列を出力しないようにします

実行結果例

元の売上データ(sample_uriage_data.csv)

purchase_date,customer_name,item_name,item_price
2024-01-15,山田太郎,apple juice,300
2024-01-16,佐藤花子,ORANGE JUICE,
2024-01-17,山田太郎,orange juice,350

元の顧客台帳(sample_customer_ledger.xlsx)

顧客名,かな,地域,メールアドレス,登録日
山田 太郎,やまだたろう,東京,yamada@example.com,44927
佐藤花子,さとうはなこ,大阪,sato@example.com,2023-01-15

出力ファイル(dump_data.csv)

purchase_date,purchase_month,item_name,item_price,顧客名,かな,地域,メールアドレス,登録日
2024-01-15,202401,APPLEJUICE,300.0,山田太郎,やまだたろう,東京,yamada@example.com,2023-01-01
2024-01-16,202401,ORANGEJUICE,350.0,佐藤花子,さとうはなこ,大阪,sato@example.com,2023-01-15
2024-01-17,202401,ORANGEJUICE,350.0,山田太郎,やまだたろう,東京,yamada@example.com,2023-01-01

ポイント

  • 商品名が大文字統一され、空白が削除されています(APPLEJUICE
  • 欠損していた価格が補完されています(2行目の350.0)
  • Excelシリアル値が日付に変換されています(44927 → 2023-01-01)
  • 顧客名の空白が削除され、マージが成功しています

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

代表的なエラー文

KeyError: 'column_name'

KeyError: 'item_price'
  • 原因: 指定した列名がDataFrameに存在しない
  • 対策: df.columns で列名一覧を確認し、正しい列名を使用する

FileNotFoundError

FileNotFoundError: [Errno 2] No such file or directory: '002_サンプル/sample_uriage_data.csv'
  • 原因: ファイルパスが間違っているか、ファイルが存在しない
  • 対策: ファイルの配置場所を確認し、相対パスまたは絶対パスを正しく指定する

ValueError: could not convert string to float

ValueError: could not convert string to float: 'abc'
  • 原因: 数値に変換できない文字列が含まれている
  • 対策: pd.to_numeric(df['column'], errors='coerce') で強制的にNaNに変換するか、事前にデータを確認する

UnicodeDecodeError

UnicodeDecodeError: 'utf-8' codec can't decode byte
  • 原因: CSVファイルの文字コードが想定と異なる
  • 対策: pd.read_csv('file.csv', encoding='shift-jis') のようにエンコーディングを指定する

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

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

print(sample_uriage_data.shape)
# 出力例: (100, 4)  # 100行、4列

columns(列名の確認)

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

head(先頭データの確認)

print(sample_uriage_data.head(3))
# 出力例:
#  purchase_date customer_name      item_name  item_price
# 0    2024-01-15      山田太郎    apple juice       300.0
# 1    2024-01-16      佐藤花子  ORANGE JUICE         NaN
# 2    2024-01-17      山田太郎   orange juice       350.0

info(データ型と欠損値の確認)

print(sample_uriage_data.info())
# 出力例:
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 100 entries, 0 to 99
# Data columns (total 4 columns):
#  #   Column          Non-Null Count  Dtype  
# ---  ------          --------------  -----  
#  0   purchase_date   100 non-null    object 
#  1   customer_name   100 non-null    object 
#  2   item_name       100 non-null    object 
#  3   item_price      95 non-null     float64
# dtypes: float64(1), object(3)

isnull().sum(列ごとの欠損値数)

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

value_counts(値の出現回数)

print(sample_uriage_data['item_name'].value_counts())
# 出力例:
# APPLEJUICE      45
# ORANGEJUICE     35
# GRAPEJUICE      20
# Name: item_name, dtype: int64

4. まとめ

本記事を通じて、以下の内容を学習しました。

  • 複数形式のデータ読み込み: CSV・Excelファイルを適切に読み込み、DataFrameとして扱う方法
  • 実践的なデータクリーニング: 日付変換、文字列正規化、欠損値補完など、実務で頻出する前処理テクニック
  • データ結合の基礎: 異なるキー名でのマージ方法と、結合後の列選択・出力までの一連の流れ

Discussion