Laravelで購入履歴からパターン分析し、DMを送るシステムをつくる

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、これはプログラムだけに限らずですが、最近は時代の変化がとても早いので、常に自分を時代に合わせていかなきゃ!と感じています。

そして、それに関連して少しずつ興味をもちはじめているのが、

マーケティングもできるエンジニア

です。

正直なところ、マーケティングはそれほど身近ではなかったのですが、どの業界にも関わっているものですので、研究も兼ねて記事を書いていくことにしました。

そこで❗

今回は、Laravelを使って「購入履歴からパターン分析し、DMを送る」システムを作ってみることにします。

※ 元ネタは『ビジネスのIQが高まる泉田式10速発想法』という本の「人は好きなことを繰り返す」というルールです。ポッドキャストもよく聞いてました!

ぜひ何かの参考になりましたら嬉しいです😊✨

「その昔、経営者目指して
マーケティング本を
読んでました」

 

開発環境: Laravel 8.x

前提として

すでにログイン機能がインストールされ、さらにテストユーザーがusersテーブルに登録されていることが前提です。

もしまだの方は、以下を参考にしてみてください。

📝 参考ページ

users テーブル・サンプル

パターン分析の方法

今回、購入履歴から見つけ出すパターンは以下2つです。

どのくらいの間隔でモノを買ってるか

過去の購入履歴から「平均でどのくらいのサイクル(日数)」でモノを買っているか計算し、最後に購入した日からその日数が経過していたら「そろそろお買い物しませんか😊」というDMを送信する。

どのカテゴリの商品を買っているか

過去に買ったことのないカテゴリの商品を紹介してもお客さんは興味は薄いはずですので、同じカテゴリの中からピックアップするようにします。(実際のDMには新着順に最大3つの商品を含める)

では、実際に作業していきましょう❗

モデル&マイグレーションをつくる

まずは商品と購入履歴データがないとパターンを見つけることはできません。
そのため、まず以下4つのテーブルを使えるようにします。

  • 商品カテゴリ: product_categories
  • 商品テーブル: products
  • 購入履歴テーブル: orders
  • 購入履歴の集計: order_summaries

では、以下のコマンドを実行してください。(順番大事です!)

php artisan make:model ProductCategory -m
php artisan make:model Product -m
php artisan make:model Order -m
php artisan make:model OrderSummary -m

すると、モデル&マイグレーションが4ペア作成されているので、まずマイグレーションを以下のように変更してください。

⚠ ご注意
今回は必要最低限必要なフィールドだけ作っています。そのため、「価格」などは用意していません。

database/migrations/****_**_**_******_create_product_categories_table.php

// 省略

public function up()
{
    Schema::create('product_categories', function (Blueprint $table) {
        $table->id();
        $table->string('name')->comment('カテゴリ名');
        $table->timestamps();
    });
}

database/migrations/****_**_**_******_create_orders_table.php

// 省略

public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id')->comment('ユーザーID');
        $table->unsignedBigInteger('product_id')->comment('商品ID');
        $table->timestamps();

        $table->foreign('user_id')->references('id')->on('users');
        $table->foreign('product_id')->references('id')->on('products');
    });
}

database/migrations/****_**_**_******_create_products_table.php

// 省略

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->unsignedBigInteger('category_id')->comment('カテゴリID');
    $table->string('name')->comment('商品名');
    $table->timestamps();

    $table->foreign('category_id')->references('id')->on('product_categories');
});

database/migrations/****_**_**_******_create_order_summaries_table.php

// 省略

public function up()
{
    Schema::create('order_summaries', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('user_id')->comment('ユーザーID');
        $table->unsignedInteger('interval_days')->comment('購入間隔');
        $table->date('dm_sending_date')->comment('DM送信予定日');
        $table->timestamps();

        $table->foreign('user_id')->references('id')->on('users');
    });
}

また、後でリレーションシップを使うので以下の変更も追加しておいてください。

app/Models/OrderSummary.php

// 省略

class OrderSummary extends Model
{
    // 省略

    // Relationship
    public function user() {

        return $this->belongsTo(User::class, 'user_id', 'id');

    }
}

app/Models/Order.php

// 省略

class Order extends Model
{
    // 省略

    // Relationship
    public function product() {

        return $this->belongsTo(Product::class, 'product_id', 'id');

    }
}

イベントをつくる

order_summariesテーブルは「購入履歴の集計データ」を管理するためにつくっています。そのため、購入されるたびに自動的にデータ更新できるようにイベントをつくっておきましょう。

以下のコマンドを実行してください。

php artisan make:event OrderSaved

すると、ファイルが作成されるので中身を次のように変更してください。

app/Events/OrderSaved.php

<?php

namespace App\Events;

use App\Models\Order;
use App\Models\OrderSummary;

// 省略

class OrderSaved
{
    // 省略

    public function __construct(Order $order)
    {
        $user_id = $order->user_id;
        $interval_days = [];
        $orders = Order::where('user_id', $user_id)
            ->orderBy('created_at', 'asc')
            ->get();

        foreach ($orders as $index => $order) {

            if($index > 0) { // データ2件目から

                $order_at = $order->created_at;
                $interval_days[] = $order_at->diffInDays($prev_order_at);

            }

            $prev_order_at = $order->created_at;

        }

        $summary = OrderSummary::firstOrNew(['user_id' => $user_id]);
        $summary->user_id = $user_id;
        $interval_days_count = count($interval_days);

        if($interval_days_count > 0) {

            $interval_days_total = array_sum($interval_days);
            $new_interval_days = round($interval_days_total / $interval_days_count);
            $summary->interval_days = $new_interval_days;
            $summary->dm_sending_date = $orders->max('created_at')->addDays($new_interval_days);
            $summary->save();

        } else if($summary->exists()) { // もしなければ削除

            $summary->delete();

        }

    }

// 以下省略

そして、OrderSavedイベントをモデルにセットします。

app/Models/Order.php

<?php

namespace App\Models;

use App\Events\OrderSaved;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;

    protected $dispatchesEvents = [
        'saved' => OrderSaved::class // 👈 ここ
    ];
}

これでOrderモデルでデータを追加&変更したら自動的に集計データを作ってくれるようになります。

テストデータをつくる

では、以下3つのテーブルにそれぞれテストデータを保存していきます。

  • product_categories: 商品カテゴリ
  • products: 商品
  • orders: 購入履歴

以下のコマンドを実行してください。

php artisan make:seed ProductCategoriesTableSeeder
php artisan make:seed ProductsTableSeeder
php artisan make:seed OrdersTableSeeder

するとSeeder用のファイルが3つ作成されていますので、それぞれ中身を次のように変更してください。

database/seeders/ProductCategoriesTableSeeder.php

<?php

namespace Database\Seeders;

use App\Models\ProductCategory;
use Illuminate\Database\Seeder;

class ProductCategoriesTableSeeder extends Seeder
{
    public function run()
    {
        $names = [
            '本・コミック・雑誌',
            'DVD・ミュージック・ゲーム',
            '家電・カメラ・AV機器',
            'パソコン・オフィス用品',
            'ホーム&キッチン・ペット・DIY'
        ];

        foreach ($names as $name) {

            $category = new ProductCategory();
            $category->name = $name;
            $category->save();

        }
    }
}

database/seeders/ProductsTableSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Product;
use App\Models\ProductCategory;
use Illuminate\Database\Seeder;

class ProductsTableSeeder extends Seeder
{
    public function run()
    {
        $category_ids = ProductCategory::pluck('id');

        for($i = 0 ; $i < 100 ; $i++) {

            $product = new Product();
            $product->category_id = $category_ids->random();
            $product->name = 'テスト商品名 - '. $i;
            $product->save();

        }
    }
}

database/seeders/OrdersTableSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Order;
use App\Models\Product;
use Illuminate\Database\Seeder;

class OrdersTableSeeder extends Seeder
{
    public function run()
    {
        $order_details = [ // キーはユーザーID
            1 => [
                'order_interval' => 1, // 購入間隔(月),
                'product_category_ids' => [1, 2] // 購入カテゴリ
            ],
            2 => [
                'order_interval' => 3, // 購入間隔(月),
                'product_category_ids' => [2, 3] // 購入カテゴリ
            ],
            3 => [
                'order_interval' => 5, // 購入間隔(月),
                'product_category_ids' => [3, 4, 5] // 購入カテゴリ
            ]
        ];

        foreach ($order_details as $user_id => $order_detail) {

            $order_count = rand(5, 25); // ユーザーごとの注文回数

            for($i = 0 ; $i < $order_count ; $i++) {

                $order_interval = $order_detail['order_interval'];
                $product_category_ids = $order_detail['product_category_ids'];
                $dt = today()
                    ->subMonth($order_interval * $i)
                    ->addDays(rand(-15, 15)); // ここは誤差をランダムで作成
                $product_ids = Product::whereIn('category_id', $product_category_ids)->pluck('id');

                $order = new Order();
                $order->user_id = $user_id;
                $order->product_id = $product_ids->random();
                $order->created_at = $dt;
                $order->save();

            }

        }

    }
}

3つのファイルが変更できたら、これらをLaravelに登録して使えるようにしておきます。

database/seeders/DatabaseSeeder.php

// 省略

public function run()
{
    // 省略

    $this->call(ProductCategoriesTableSeeder::class);
    $this->call(ProductsTableSeeder::class);
    $this->call(OrdersTableSeeder::class);
}

では、データベース周りの設定は完了です。
以下のコマンドでDBを再構築してください。

php artisan migrate:fresh --seed

うまくいけば、4つのテーブルが作成されてその全てにデータが登録されているはずです。

では、order_summariesテーブルだけはイレギュラーなので確認してみましょう。

うまく「購入間隔」と「DM送信予定日」が保存されてるようです👍

※ なお、OrdersTableSeeder内で注文回数や購入日時にランダムな誤差を与えているので同じデータにはなっていないと思います。

DMを送信する部分をつくる

メール部分をつくる

今回はシンプルにメールでDMを送信することにします。
以下のコマンドを実行してください。

php artisan make:mail OrderCycleHasCome

するとファイルが作成されるので中身を次のようにします。

app/Mail/OrderCycleHasCome.php

<?php

namespace App\Mail;

use App\Models\User;
use Illuminate\Support\Collection;

// 省略

class OrderCycleHasCome extends Mailable
{
    // 省略

    private $products, $user;

    public function __construct(Collection $products, User $user)
    {
        $this->products = $products;
        $this->user = $user;
    }

    public function build()
    {
        $from = 'info@example.com';
        $app_name = config('app.name');

        return $this
            ->to($this->user->email)
            ->from($from, $app_name)
            ->subject('新着商品のご紹介')
            ->view('emails.order_cycle_has_come')
            ->with([
                'products' => $this->products,
                'user' => $this->user,
                'app_name' => $app_name
            ]);
    }
}

次に、メール本文のテンプレートです。

resources/views/emails/order_cycle_has_come.blade.php

<strong>{{ $user->name }} 様</strong>、いつも当店でご購入いただきありがとうございます!<br><br>

本日は <strong>{{ $user->name }} 様</strong> におすすめの新着商品をご紹介させていただきます &#x1F6CD;<br><br>

<hr>

@foreach($products as $product)
    <a href="{{ url('products/'. $product->id) }}">{{ $loop->iteration }}. {{ $product->name }}</a><br>
    <img src="https://via.placeholder.com/360x180?text=Test%20Image"><br>
    <hr>
@endforeach

<br>

【{{ $app_name }}】<br>
<a href="{{ url('/') }}">{{ url('/') }}</a>

※ 本来、広告メールを送信する際はオプトアウトの方法を記述する必要があります。詳しくは、ちなみに: 特定電子メール法 をご覧ください。

コマンドをつくる

毎回毎回手動ではめんどうですので、Laravelに独自のコマンドをつくり、そのコマンドをcrontabで定期的に自動実行することにします。

以下のコマンドを実行してください。

php artisan make:command SendDmCommand

するとファイルが作成されますので、中身を以下のように変更します。コマンドは作っただけでは利用できませんので、以下のようにしてLaravelへ登録します。

<?php

namespace App\Console\Commands;

use App\Mail\OrderCycleHasCome;
use App\Models\Order;
use App\Models\OrderSummary;
use App\Models\Product;
use Illuminate\Console\Command;

class SendDmCommand extends Command
{
    // 省略

    protected $signature = 'send:dm';

    // 省略

    protected $description = 'Send DM';

    // 省略

    public function handle()
    {
        $today = today();
        $summaries = OrderSummary::where('dm_sending_date', $today)->get();

        foreach($summaries as $summary) {

            $user_id = $summary->user_id;
            $orders = Order::where('user_id', $user_id)->get();
            $category_ids = $orders->pluck('product.category_id')->unique();
            $product_ids = $orders->pluck('product_id')->unique();

            $products = Product::whereIn('category_id', $category_ids)
                ->whereNotIn('id', $product_ids)
                ->latest()
                ->take(3)
                ->get();
            \Mail::send(new OrderCycleHasCome($products, $summary->user));

        }

        $this->info('Done!');
    }
}

app/Console/Kernel.php

<?php

namespace App\Console;

use App\Console\Commands\SendDmCommand;

// 省略

class Kernel extends ConsoleKernel
{
    // 省略
    protected $commands = [
        SendDmCommand::class
    ];

これで、php artisan send:dmコマンドが使えるようになりました。

ちなみに: 特定電子メール法

なお、現在は「特定電子メール法」によって広告メールを送信する際は、

  • オプトイン
  • オプトアウト

の機能が使えないといけませんのでご注意ください。
詳しくは以下のページをご覧ください。

📝 【Laravel】オプトイン/オプトアウトのメール送信機能

テストしてみる

では、order_summariesテーブルのdm_sending_dateを1件今日に手動で変更し、送信テストをしてみましょう❗

では、作成した以下のコマンドを実行してみます。

php artisan send:dm

すると・・・・・・

はい❗

うまくメール送信できました。
成功です😊✨

※ 自動的にタイマー実行するには crontab などにphp artisan send:dmが実行されるようにしてください。

企業様へのご提案

今回は「購入周期」と「興味のあるカテゴリ」だけで集計をしましたが、それ以外にも以下のような集計方法が考えられます。

  • 商品ごとに平均どのくらいの金額を使っているかを計算し、より近い商品をすすめる
  • お客さんによってライフスタイルが違うのでDMを送信すべき時間帯を過去データから割り出す
  • 商品に色データを用意し、「好きな色」が含まれている商品ばかりをおすすめする
  • 購入後に投稿したレビュー内容(スターなど)を使って、逆に「おすすめすべきでない」商品を割り出す
  • プレゼント用として購入されたものはおすすめからは除外する

などなど。

また、さらに発展させてDMの「クリック率」や「購入率」を検証し、より買ってもらえる集計を探すのもひとつの手かもしれません。

ぜひこういったシステムで販促をお考えの場合は、お問い合わせからご相談ください。m(_ _)m

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回は「プログラムとマーケティングの融合」をテーマに記事を書いてみました。

過去にはいくつもマーケティング本を買って読みましたが、ほぼ大掃除で捨ててしまいました。

ただ、その中には「この本だけは捨てちゃダメだ!」と、手元に残っているものがあるのでまた読み返してみようと思います。

それにしても、ずっと昔に読んだ本でも感銘を受けた部分は覚えているものですね。

ぜひ皆さんも、プログラム & マーケティングで何か機能を考えてみてくださいね。

ではでは〜❗

「孫子の兵法も
もう一回読もうかな。
戦わずに勝ちたい(笑)」

このエントリーをはてなブックマークに追加       follow us in feedly