顧客セグメント分析のやり方|購買データをk-meansでグループ分けし、施策と効果検証につなげる手順(Python)

顧客分析で機械学習を使う一番の目的は、全員に同じ施策を打つのをやめ、「買い方が似ている顧客のグループ」ごとに打ち手を変えることです。手順は、購買ログを顧客ごとに集計し、k-meansでグループに分け、グループの特徴を表にして施策を決め、施策を送らない群(ホールドアウト)と比べて効果を測る、の4段階です。

この記事では、架空の購買ログ600人分をPythonで作り、この4段階を実際に動かします。出力はすべて実行した結果です(Python 3.12、pandas 3.0.6、scikit-learn 1.9.1、2026年10月17日確認)。データは説明のために乱数で作ったもので、数字そのものに意味はありません。見てほしいのは手順と、結果の読み方です。

RFM分析だけを深掘りしたい場合はRFM分析のやり方とスコアの付け方、解約しそうな顧客の予測はチャーン予測の作り方と評価方法、次に何を買うかの予測は購買行動を予測する方法にまとめています。この記事は、それらの手前にある「顧客をグループに分けて、施策につなげる」ところを扱います。

顧客セグメント分析の全体像

セグメント分析は、モデルを作って終わりではありません。施策に使えて、効果を測れるところまでが一続きです。

段階 やること この記事のコード
1. 集計 1行=1回の購入、という購買ログを、1行=1人の顧客テーブルにまとめる RFMとカテゴリ構成比を作る
2. 分割 特徴量をそろえて、k-meansで顧客をk個のグループに分ける シルエット係数でkの候補を見る
3. 解釈 グループごとの人数・中央値・比率を表にして、名前と施策を決める プロファイル表を出す
4. 検証 施策を送る群と送らない群に無作為に分け、あとで差を比べる ホールドアウト群を作る

年齢・性別・居住地のような属性で分ける方法もありますが、同じ30代女性でも、毎週食品を買う人と年に1回家電を買う人では、効く施策がまったく違います。機械学習でのセグメント分析は、「何を・どれくらいの頻度で・いくら買っているか」という行動のデータからグループを作るところに強みがあります。

手順1:購買ログを顧客ごとの表にまとめる

多くの会社で手元にあるのは、POSやECの注文データのような「購入1回ごとの記録」です。まずはその形の架空データを作ります。顧客ごとに隠れた買い方のタイプを決めて、購入回数・最後に買った時期・よく買うカテゴリをタイプごとに変えています。

import numpy as np
import pandas as pd

rng = np.random.default_rng(0)

# 架空の購買ログ(1行=1回の購入)を作る
n_customers = 600
types = rng.choice(["まとめ買い", "食品中心", "たまに高額", "休眠"], size=n_customers, p=[0.2, 0.4, 0.15, 0.25])
rows = []
for cid, t in enumerate(types):
    n = {"まとめ買い": 18, "食品中心": 8, "たまに高額": 3, "休眠": 2}[t]
    last = {"まとめ買い": 20, "食品中心": 40, "たまに高額": 90, "休眠": 300}[t]
    for _ in range(rng.poisson(n) + 1):
        day = int(rng.integers(last, 365))
        if t == "食品中心":
            cat = rng.choice(["食品", "日用品", "家電"], p=[0.8, 0.18, 0.02])
        elif t == "たまに高額":
            cat = rng.choice(["食品", "日用品", "家電"], p=[0.2, 0.2, 0.6])
        else:
            cat = rng.choice(["食品", "日用品", "家電"], p=[0.45, 0.45, 0.1])
        price = {"食品": 2500, "日用品": 3000, "家電": 30000}[cat] * rng.uniform(0.5, 1.5)
        rows.append({"customer_id": cid, "days_ago": day, "category": cat, "amount": round(price)})
log = pd.DataFrame(rows)
print(log.head())
print("購入件数:", len(log), "/ 顧客数:", log["customer_id"].nunique())
log.to_csv("log.csv", index=False)
   customer_id  days_ago category  amount
0            0       190       家電   34707
1            1       274       食品    2302
2            1       255       食品    2317
3            1       162       食品    2194
4            1       348       食品    1610
購入件数: 5143 / 顧客数: 600

days_agoは「何日前に買ったか」です。これを顧客ごとに集計して、次の6つの列を作ります。

  • recency:最後の購入から何日たったか(小さいほど最近)
  • frequency:購入回数
  • monetary:合計金額
  • share_食品・share_日用品・share_家電:金額のうち各カテゴリが占める割合

最初の3つがいわゆるRFMです。RFMだけだと「よく買う人」と「たまにしか買わない人」は分けられても、「何を買う人か」は分かりません。カテゴリの構成比を足すと、送るクーポンやおすすめ商品を選ぶ手がかりになります。

import numpy as np
import pandas as pd

log = pd.read_csv("log.csv")

# 顧客ごとに1行へまとめる(RFM+カテゴリ構成比)
customers = log.groupby("customer_id").agg(
    recency=("days_ago", "min"),      # 最後の購入から何日たったか
    frequency=("days_ago", "size"),   # 購入回数
    monetary=("amount", "sum"),       # 合計金額
)
share = pd.crosstab(log["customer_id"], log["category"], values=log["amount"],
                    aggfunc="sum", normalize="index").fillna(0)
customers = customers.join(share.add_prefix("share_"))
print(customers.head().round(2))
print(customers.describe().loc[["mean", "50%", "max"]].round(1))
customers.to_csv("customers.csv")
             recency  frequency  monetary  share_家電  share_日用品  share_食品
customer_id
0                190          1     34707      1.00       0.00      0.00
1                162          5     11345      0.00       0.00      1.00
2                 43         18     61889      0.25       0.41      0.34
3                 29         16     90263      0.54       0.32      0.14
4                328          6     51763      0.75       0.00      0.25
      recency  frequency  monetary  share_家電  share_日用品  share_食品
mean    147.7        8.6   46250.3       0.3        0.2       0.4
50%      95.0        7.0   31533.5       0.0        0.2       0.4
max     360.0       29.0  230988.0       1.0        1.0       1.0

合計金額の平均(46,250)が中央値(31,534)よりかなり大きく、最大は23万を超えています。一部の顧客が大きく引き上げている、右に長い分布です。このまま使うと、k-meansが金額の大きい少数の顧客に振り回されるので、次の手順で対数をとります。

手順2:k-meansで顧客をグループに分ける

k-meansは「点と点の距離」でグループを作るので、単位の違う列(日数・回数・円・割合)をそのまま混ぜると、数字の大きい金額の列だけでグループが決まってしまいます。そこで、偏りの大きい回数と金額はlog1p(1を足して対数をとる)で縮め、全部の列をStandardScalerで平均0・標準偏差1にそろえます。標準化の考え方は正規化と標準化の違いで詳しく扱っています。

グループの数kは自分で決める必要があります。目安として、各点が自分のグループにどれだけしっくり収まっているかを表すシルエット係数を、kを変えながら計算します。scikit-learnの説明では、シルエット係数は-1から1の値をとり、1に近いほど良く、0付近はグループ同士が重なっていることを示します。

import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import StandardScaler

customers = pd.read_csv("customers.csv", index_col="customer_id")

X = customers.copy()
X["frequency"] = np.log1p(X["frequency"])  # 偏りの大きい列は対数をとる
X["monetary"] = np.log1p(X["monetary"])
X_scaled = StandardScaler().fit_transform(X)

for k in range(2, 7):
    labels = KMeans(n_clusters=k, n_init=10, random_state=0).fit_predict(X_scaled)
    print(f"k={k}  シルエット係数={silhouette_score(X_scaled, labels):.3f}")
k=2  シルエット係数=0.376
k=3  シルエット係数=0.418
k=4  シルエット係数=0.468
k=5  シルエット係数=0.475
k=6  シルエット係数=0.424

数字だけ見るとk=5がいちばん高いのですが、k=4との差は0.007しかありません。こういうときは、両方のグループの中身を見て、施策として意味のある分け方かどうかで決めます。

なお、KMeansのn_init(初期値を変えて何回やり直すか)は、現行のscikit-learnでは既定値が'auto'です。結果を再現できるように、この記事ではn_init=10とrandom_state=0を明示しています。

手順3:グループの特徴を表にして、施策を決める

k=4とk=5で、グループごとの人数と、各列の中央値(比率は平均)を並べます。平均ではなく中央値にしているのは、手順1で見たように金額が一部の顧客に引っぱられるからです。

import numpy as np
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

customers = pd.read_csv("customers.csv", index_col="customer_id")
X = customers.copy()
X["frequency"] = np.log1p(X["frequency"])
X["monetary"] = np.log1p(X["monetary"])
X_scaled = StandardScaler().fit_transform(X)

for k in (4, 5):
    customers["segment"] = KMeans(n_clusters=k, n_init=10, random_state=0).fit_predict(X_scaled)
    profile = customers.groupby("segment").agg(
        人数=("recency", "size"),
        最終購入から日数=("recency", "median"),
        購入回数=("frequency", "median"),
        合計金額=("monetary", "median"),
        家電の比率=("share_家電", "mean"),
        食品の比率=("share_食品", "mean"),
    ).round(2)
    print(f"--- k={k}")
    print(profile)
--- k=4
          人数  最終購入から日数  購入回数     合計金額  家電の比率  食品の比率
segment
0        127     316.0   3.0   7331.0   0.00   0.52
1        197      65.0   9.0  24015.0   0.00   0.78
2        139     178.0   4.0  50989.0   0.89   0.05
3        137      36.0  18.0  88915.0   0.50   0.25
--- k=5
          人数  最終購入から日数  購入回数     合計金額  家電の比率  食品の比率
segment
0        195      65.0   9.0  24256.0   0.00   0.78
1        139     178.0   4.0  50989.0   0.89   0.05
2         54     317.5   3.0   7286.0   0.00   0.84
3         75     313.0   3.0   7800.0   0.00   0.29
4        137      36.0  18.0  88915.0   0.50   0.25

k=4の4グループは、次のように読めます。グループの番号は実行するたびに並びが変わることがあるので、番号ではなく中身で名前を付けるのがコツです。

k=4のグループ 表から読めること 名前の例 施策の例
3(137人) 最終購入が36日前、購入18回、金額も最大。家電も食品も買う 常連 値引きより先行案内や会員特典。値引きを配ると、もともと買う人に割引するだけになりやすい
1(197人) 65日前、9回。金額の8割近くが食品 食品の定期客 まとめ買いや定期便の案内、日用品との組み合わせ提案
2(139人) 178日前、4回。金額の9割近くが家電 家電スポット客 買った家電の消耗品・保証・買い替え時期の案内
0(127人) 316日前、3回、金額も小さい 休眠客 再来店のきっかけづくり。費用をかけすぎない

k=5にすると、k=4の休眠客(0番)が、食品が中心の54人(2番)とそれ以外の75人(3番)に分かれるだけで、ほかの3グループはほぼ同じです。休眠客に食品と日用品で別々の施策を打つ予定がないなら、k=4で十分です。逆に、休眠客への施策を分けたいならk=5を選びます。kは「施策をいくつ用意できるか」で決めてよいというのが、実務での落としどころです。

もう一つ大切なのは、作ったグループを担当者が見て「確かにこういうお客さんがいる」と言えるかどうかです。説明できないグループは、施策も決められません。

手順4:施策の効果を測れるようにしておく

セグメントごとに施策を打ったあと、「売上が上がったから効果があった」とは言い切れません。季節や新商品の影響でも売上は動くからです。そこで、各グループの一部を無作為に選んで施策を送らない群(ホールドアウト)にしておき、あとで「送った群」と「送らなかった群」の購入率や購入額を比べます。

# 手順3のコードの続き
# 施策を送る群と送らない群(ホールドアウト)を、セグメントごとに無作為に分ける
customers["segment"] = KMeans(n_clusters=4, n_init=10, random_state=0).fit_predict(X_scaled)
rng = np.random.default_rng(1)
customers["group"] = np.where(rng.random(len(customers)) < 0.8, "送る", "送らない")
print(pd.crosstab(customers["segment"], customers["group"]))
group    送らない   送る
segment
0          23  104
1          49  148
2          19  120
3          27  110

この表のように、どのグループにも送らない群が入っていることを確認しておきます。グループが小さいと、送らない群が数十人しかいなくなり、差が出ても偶然と区別できません。比べるときは、2つの群の購入率の差が偶然で説明できる範囲かどうかを、統計的な検定(A/Bテストと同じ考え方)で確かめます。施策の前に「何を・いつ・どの指標で比べるか」を決めておくことが、あとから都合のよい数字だけを選ばないための約束になります。

やってはいけないこと・注意点

  • グループを一度作って放置する:顧客の買い方は変わります。休眠客が戻ってきたり、常連が離れたりするので、月ごとや四半期ごとに作り直し、グループの人数がどう動いたかを見ます。
  • 同じデータで「予測」と「評価」をする:セグメントの次に離脱予測などの教師あり学習をするなら、学習用と評価用のデータを分けます。
  • 個人情報の扱いを確認しない:購買履歴は個人情報と結びついていることが多いデータです。利用目的の範囲内で使っているか、外部のツールにデータを渡してよいかなどは、個人情報保護委員会の法令・ガイドラインのページを確認し、社内の担当者と相談してから進めます。

学び方のヒント

セグメント分析で使った技術は、集計(pandas)、標準化、k-means、シルエット係数、無作為な割り付けと検定です。どれも単独で学べますが、実データで最初から最後まで通してみると、どこで判断が必要になるかが見えてきます。k-meansそのものや、ほかのクラスタリング手法(階層型・DBSCANなど)との違いはクラスタリングの手法と比較で解説しています。

体系的に学ぶなら、テックジムのAIエンジニア養成コース(16章で機械学習とディープラーニングを学ぶコース)では、第1章でk-meansなどの教師なし学習を、第5章「実践ビジネスデータ分析」で入会・継続・退会の予測を、第14章でA/Bテストを使った効果検証を扱っています。

よくある質問

Q. RFM分析とk-meansのクラスタリングは、どちらを使えばいいですか?

RFMのスコアで分ける方法は、ルールが単純で説明しやすいのが利点です。k-meansは、RFMに購入カテゴリの比率などを足しても、データから自然なまとまりを見つけられます。まずRFMで全体をつかみ、施策を細かく分けたくなったらk-meansを使う、という順番が無理のない進め方です。

Q. グループの数kはどう決めますか?

シルエット係数などの指標で候補を絞り、そのうえで各グループの中身を表にして、施策として分ける意味があるかで決めます。指標の差がわずかなら、用意できる施策の数に合わせて小さいほうを選んでかまいません。

Q. 顧客データは何件くらいあれば分析できますか?

決まった下限はありませんが、施策の効果を比べるには、各グループの中に「送る群」と「送らない群」をそれぞれ十分な人数で作る必要があります。グループが数十人しかいない場合は、グループの数を減らすほうが結果を判断しやすくなります。

Q. 金額のような偏った列は、そのまま使ってもいいですか?

k-meansは距離でグループを作るので、一部の高額顧客に結果が引っぱられます。この記事のように対数をとってから標準化するか、RFM分析のように順位やスコアに置き換えてから使うと安定します。

参考にした一次情報

機械学習の全体像は機械学習とは?仕組み・種類・できることを簡単に解説|Pythonで動く例つきにまとめています。

プログラミング学習に興味がある方は、まずは無料体験会へ

無料体験会・お申し込みはこちら