🐍
(学習ログ)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_on
とright_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