🐍

(学習ログ)Python025:Pandasで複数テーブルを結合し顧客データを分析する

に公開

1. はじめに

複数のマスターテーブルを結合し、顧客の利用状況を多角的に分析する一連の流れを解説します。

実務でよく使われるデータ結合(merge)日付フィルタリンググループ集計統計量の算出といった基本技術を、顧客分析という具体的な場面で整理します。

2. サンプルコード

コード全文

import pandas as pd

# データ読み込み
sample_usagelog = pd.read_csv('003_サンプル/usage_log.csv')
sample_customer_master = pd.read_csv('003_サンプル/customer_master.csv')
sample_class_master = pd.read_csv('003_サンプル/class_master.csv')
sample_campaign_master = pd.read_csv('003_サンプル/campaign_master.csv')

# テーブル統合:顧客マスター + クラスマスター
sample_customer_join = pd.merge(sample_customer_master, sample_class_master, on="class", how="left")

# さらにキャンペーンマスターを結合
sample_customer_join = pd.merge(sample_customer_join, sample_campaign_master, on="campaign_id", how="left")

# 各属性での顧客数集計
sample_customer_join.groupby("class_name").count()["customer_id"]
sample_customer_join.groupby("campaign_name").count()["customer_id"]
sample_customer_join.groupby("gender").count()["customer_id"]
sample_customer_join.groupby("is_deleted").count()["customer_id"]

# start_date列を文字列から日付型に変換
sample_customer_join["start_date"] = pd.to_datetime(sample_customer_join["start_date"])

# 2018年4月1日より後に開始した顧客のみを抽出
sample_customer_start = sample_customer_join.loc[sample_customer_join["start_date"] > pd.to_datetime("20180401")]

# 終了日での絞り込み:2019年3月31日以降または終了日がNULL
sample_customer_join["end_date"] = pd.to_datetime(sample_customer_join["end_date"])
sample_customer_newer = sample_customer_join.loc[(sample_customer_join["end_date"] >= pd.to_datetime("20190331")) | (sample_customer_join["end_date"]).isna()]

# 絞り込み後のデータ確認
sample_customer_newer["end_date"].unique()
sample_customer_newer.groupby("class_name").count()["customer_id"]
sample_customer_newer.groupby("campaign_name").count()["customer_id"]
sample_customer_newer.groupby("gender").count()["customer_id"]

# 利用ログの月次集計
sample_usagelog["usedate"] = pd.to_datetime(sample_usagelog["usedate"])
sample_usagelog["年月"] = sample_usagelog["usedate"].dt.strftime("%Y%m")
sample_usagelog_months = sample_usagelog.groupby(["年月", "customer_id"], as_index=False).count()
sample_usagelog_months.rename(columns={"log_id": "count"}, inplace=True)
del sample_usagelog_months["usedate"]

# 顧客別の利用統計量を算出
sample_usagelog_customer = sample_usagelog_months.groupby("customer_id")["count"].agg(["mean", "median", "max", "min"])
sample_usagelog_customer = sample_usagelog_customer.reset_index(drop=False)

コード解説

ブロック1:データ読み込みとテーブル結合

何をしているか:4つのCSVファイルを読み込み、pd.merge()で段階的に結合しています。

なぜそうするか:顧客マスターにはclasscampaign_idというコード値しか入っていません。そのコード値に対応する名称や詳細情報を取得するために、クラスマスターやキャンペーンマスターと結合する必要があります。これにより、「クラスA」ではなく「プレミアム会員」といった分かりやすい名称で分析できるようになります。

sample_customer_join = pd.merge(sample_customer_master, sample_class_master, on="class", how="left")
sample_customer_join = pd.merge(sample_customer_join, sample_campaign_master, on="campaign_id", how="left")

初心者ポイントhow="left"は左結合を意味します。左側のテーブル(ここでは顧客マスター)の全レコードを残し、右側のテーブルから一致する情報を追加します。一致しない場合はNaNになりますが、顧客データは失われません。

ブロック2:属性別の顧客数集計

何をしているか:結合後のデータをgroupby()で分類し、各カテゴリーの顧客数をカウントしています。

なぜそうするか:クラス別、キャンペーン別、性別など、異なる切り口で顧客の分布を把握することで、マーケティング戦略やサービス改善のヒントを得られます。

sample_customer_join.groupby("class_name").count()["customer_id"]
sample_customer_join.groupby("gender").count()["customer_id"]

ブロック3:日付フィルタリングによる対象顧客の絞り込み

何をしているか:文字列の日付をpd.to_datetime()で日付型に変換し、条件に合う顧客だけを抽出しています。

なぜそうするか:特定期間のアクティブな顧客のみを分析対象にするためです。例えば、2018年4月以降に入会し、2019年3月末時点でも契約継続中(または終了日がNULL)の顧客は、直近で活動している重要な顧客層と言えます。

sample_customer_join["start_date"] = pd.to_datetime(sample_customer_join["start_date"])
sample_customer_newer = sample_customer_join.loc[(sample_customer_join["end_date"] >= pd.to_datetime("20190331")) | (sample_customer_join["end_date"]).isna()]

初心者ポイント:条件式で|(パイプ)は「または」を意味します。また、.isna()でNULL値(終了日未設定=継続中)を判定できます。条件式全体を()で囲むことを忘れずに。

ブロック4:利用ログの月次集計

何をしているか:利用ログから年月を抽出し、顧客ごと・月ごとの利用回数を集計しています。

なぜそうするか:日次データのままでは細かすぎて傾向が見えません。月単位に集約することで、顧客の利用パターンや継続性を把握しやすくなります。

sample_usagelog["年月"] = sample_usagelog["usedate"].dt.strftime("%Y%m")
sample_usagelog_months = sample_usagelog.groupby(["年月", "customer_id"], as_index=False).count()

初心者ポイント.dt.strftime("%Y%m")は日付オブジェクトから年月の文字列(例:202410)を抽出する便利な記法です。as_index=Falseを指定すると、groupbyのキーが通常の列として残ります。

ブロック5:顧客別の統計量算出

何をしているか:各顧客の月次利用回数から、平均・中央値・最大・最小といった統計量を一度に計算しています。

なぜそうするか:単純な合計だけでなく、利用の安定性(中央値)や最大利用(ピーク)を知ることで、ヘビーユーザーとライトユーザーを区別できます。これにより、顧客セグメントに応じた施策を検討できます。

sample_usagelog_customer = sample_usagelog_months.groupby("customer_id")["count"].agg(["mean", "median", "max", "min"])
sample_usagelog_customer = sample_usagelog_customer.reset_index(drop=False)

実行結果例

クラス別顧客数の集計結果

sample_customer_join.groupby("class_name").count()["customer_id"]
class_name
スタンダード会員    2845
プレミアム会員      1203
ゴールド会員         452
Name: customer_id, dtype: int64

絞り込み後の性別分布

sample_customer_newer.groupby("gender").count()["customer_id"]
gender
F    1876
M    2134
Name: customer_id, dtype: int64

顧客別利用統計の結果(最初の5行)

sample_usagelog_customer.head()
   customer_id   mean  median   max  min
0         1001  12.5    12.0    18    7
1         1002   8.3     8.0    14    3
2         1003  15.7    16.0    21   11
3         1004   5.2     5.0     9    2
4         1005  10.8    11.0    15    6

この結果から、顧客1003は平均15.7回/月と高頻度で利用している一方、顧客1004は平均5.2回/月とやや少なめであることが分かります。

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

代表的なエラー

エラー1:KeyError: 'column_name'

KeyError: 'customer_id'

原因:指定した列名がデータフレームに存在しない。スペルミスや結合前にアクセスしようとしている可能性があります。

対処法df.columnsで列名を確認しましょう。

print(sample_customer_join.columns)

エラー2:TypeError: Cannot compare tz-naive and tz-aware datetime-like objects

TypeError: Cannot compare tz-naive and tz-aware datetime-like objects

原因:タイムゾーン情報の有無が異なる日付同士を比較しようとしています。

対処法pd.to_datetime()にタイムゾーン指定を統一するか、.tz_localize(None)でタイムゾーン情報を削除します。

sample_customer_join["start_date"] = pd.to_datetime(sample_customer_join["start_date"]).dt.tz_localize(None)

エラー3:ValueError: You are trying to merge on object and int64 columns

ValueError: You are trying to merge on object and int64 columns

原因:結合キーの型が左右のテーブルで異なっています(片方が文字列、もう片方が数値など)。

対処法:結合前に型を揃えます。

sample_customer_master["campaign_id"] = sample_customer_master["campaign_id"].astype(str)
sample_campaign_master["campaign_id"] = sample_campaign_master["campaign_id"].astype(str)

よく使う確認メソッド

1. データ型の確認:df.dtypes

sample_customer_join.dtypes
customer_id       int64
start_date       object
end_date         object
class            object
dtype: object

日付列がobject型(文字列)になっていたら、pd.to_datetime()での変換が必要です。

2. 欠損値の確認:df.isnull().sum()

sample_customer_join.isnull().sum()
customer_id       0
start_date        0
end_date        234
class             0
dtype: int64

end_dateに234件の欠損があることが分かります。継続中の顧客と推測できます。

3. ユニーク値の確認:df['column'].unique()

sample_customer_newer["end_date"].unique()
array(['2019-03-31', '2019-05-15', '2019-08-20', NaT], dtype='datetime64[ns]')

フィルタリング後に意図した日付範囲になっているか確認できます。

4. データの先頭確認:df.head()

sample_usagelog_customer.head()
   customer_id   mean  median   max  min
0         1001  12.5    12.0    18    7
1         1002   8.3     8.0    14    3

集計結果が正しく計算されているか、数値の妥当性を目視で確認できます。

4. まとめ

  • 複数テーブルの結合方法pd.merge()を使い、マスターデータを段階的に結合して分析用データセットを作成する手法
  • 日付型への変換とフィルタリングpd.to_datetime()で文字列を日付型に変換し、条件式で期間指定して必要なデータを抽出する技術
  • グループ集計と統計量の算出groupby()agg()を組み合わせ、顧客別・属性別に多角的な統計情報を取得する方法
  • 実務的なエラー対処:型の不一致や欠損値など、実際の分析で遭遇しやすいエラーの原因と解決策

Discussion