2017-11-15

メルカリの出品データをマイニングして、適正価格を分析してみた

メルカリでデータマイニングしたら面白いんでないか




ここ最近、初めてメルカリを使ってみて、ユーザービリティに感動したのですが、それ以上に「データ面白い!」となりました。


というのも、値が付きにくいものに、価格がつけられていて、かつ同じような商品のサンプルデータが豊富だからです。


例えば、普通に中古品でルンバが欲しかったとして、電化製品を中古屋に行ってみてください。そこにルンバはあっても10種類ほどでしょう。中古のルンバの相場が、そこにいっただけではわかりません。


しかし、メルカリのデータを利用すると、一体全体その商品の中古相場はどのくらいなのかを特定することができます。


また、メルカリには購買済みか否かを判断する条件が存在するので、どのくらいの値段で売れば売れるかを可視化できるはずです。



分かったらうれしいこと


ということで、メルカリのデータには、

  • 価格
  • 購買済みか否か
  • タイトル
の情報があります。これらのデータを分析して、以下の問いが分かったら面白そうです。今回は、個人的に欲しいルンバを例に分析してみたいと思います。





(1) ヒトはルンバにいくらまで払うか

  • 価格

このデータを使えば、ルンバの最高値、最安値がわかります。また、ヒストグラムを描けば、ルンバの相場もかなり視覚的に可視化することができます。人がルンバに代替いくらまでならお金をかけるのかがわかります。

(2) いくらルンバの値段を上げると、いくら購入率が落ちるのか

  • 価格
  • 購入済みか否か

この2つのデータがあれば、どのくらいの値段で売れば、何割の確率で売れるかがわかるはずです。仮説としては、値段が高くなればなるほど、購入率は低下すると思われます。このデータマイニングの結果、今後どの値段にしたら何割の確率で買ってくれるか予測できるモデルができたら、めっちゃくちゃ面白いですよね。


売る側としたら、値段を上げすぎずに売れますし、買う側としたら、その商品の潜在的な潜在人気度がわかるはずです。


手法


複雑な分析方法は使いません(使えません)ので、ルンバで検索したデータのうち、

  • ルンバの付属品(クリーナー、充電器等)
  • ジャンク品・故障品

を除外し、Pythonでデータ収集、その結果をTableauというBIツールを利用して、データ分析してみました。具体的にはヒストグラムや相関分析、また検定などもしてみようと思います。

ソースコードと、データの処理

まずはPythonでWebスクレイピングします。その時のコードがこちら。


mercari.py


import sys
import os
from selenium import webdriver
import pandas
import time

#Googleにアクセスして、タイトルをとってこれるかテスト
browser = webdriver.Chrome(executable_path='/mnt/c/workspace/pydev/chromedriver.exe')

#1 

args = sys.argv
df = pandas.read_csv('default.csv', index_col=0)

#2

query = args[1]

#3 

browser.get("https://www.mercari.com/jp/search/?sort_order=price_desc&keyword={}&category_root=&brand_name=&brand_id=&size_group=&price_min=&price_max=".format(query))

#4

page = 1

#5

while True: #continue until getting the last page

    #5-1

    if len(browser.find_elements_by_css_selector("li.pager-next .pager-cell:nth-child(1) a")) > 0:
        print("######################page: {} ########################".format(page))
        print("Starting to get posts...")

        #5-1-2

        posts = browser.find_elements_by_css_selector(".items-box")

        #5-1-3

        for post in posts:
            title = post.find_element_by_css_selector("h3.items-box-name").text

            #5-1-3-1

            price = post.find_element_by_css_selector(".items-box-price").text
            price = price.replace('¥', '')

            #5-1-3-2

            sold = 0
            if len(post.find_elements_by_css_selector(".item-sold-out-badge")) > 0:
                sold = 1

            url = post.find_element_by_css_selector("a").get_attribute("href")
            se = pandas.Series([title, price, sold,url],['title','price','sold','url'])
            df = df.append(se, ignore_index=True)

        #5-1-4

        page+=1

        btn = browser.find_element_by_css_selector("li.pager-next .pager-cell:nth-child(1) a").get_attribute("href")
        print("next url:{}".format(btn))
        browser.get(btn)
        print("Moving to next page......")

    #5-2

    else:
        print("no pager exist anymore")
        break
#6
df.to_csv("{}.csv".format(query))
print("DONE")

#1コマンドライン引数の受け取りと、CSVの読み込み


#1 

args = sys.argv
df = pandas.read_csv('default.csv', index_col=0)

sys.argvは、コマンドライン引数で値がとってくれるようにします。

$ mercari.py ルンバ 
で実行すると、ルンバの検索結果を取ってこれるようにします。

defrault.csvはすでに、


  • タイトル
  • URL
  • 購入済みか
  • 価格
がすでに書かれているものとなっております。

#2 コマンドライン引数の取得


#2

query = args[1]

ここで一つ目のコマンドライン引数の検索キーワードを取得し、queryに代入します。

#3 検索キーワードで検索


検索キーワードをURLに代入し、検索を行います。
browser.get()は、引数のURLにアクセスするメソッドです。

#3 

browser.get("https://www.mercari.com/jp/search/?sort_order=price_desc&keyword={}&category_root=&brand_name=&brand_id=&size_group=&price_min=&price_max=".format(query))

#4

page = 1

#5 ページ上の商品データをすべて取得し、最後のページまで一つずつ移動する

検索結果、(#5-1)もしページャーで「次へ」のボタンが存在する場合は、(#5-2)中の商品のDIVを全て取得し、(#5-1-3)その中の(#5-1-3-1)価格を取得します。(#5-1-3-1)購入済みかいなかを確認し、購入していれば1,していなければ0を代入します。その後pandasを利用して、データフレームを作成し、CSVに落とし込みます。(#5-1-4)その後ページャーの次へをクリックし、#5-1から同様の処理をループします。(#5-2)ページャーが存在しなくなったら、処理を終了します。

#5

while True: #continue until getting the last page

    #5-1

    if len(browser.find_elements_by_css_selector("li.pager-next .pager-cell:nth-child(1) a")) > 0:
        print("######################page: {} ########################".format(page))
        print("Starting to get posts...")

        #5-1-2

        posts = browser.find_elements_by_css_selector(".items-box")

        #5-1-3

        for post in posts:
            title = post.find_element_by_css_selector("h3.items-box-name").text

            #5-1-3-1

            price = post.find_element_by_css_selector(".items-box-price").text
            price = price.replace('¥', '')

            #5-1-3-2

            sold = 0
            if len(post.find_elements_by_css_selector(".item-sold-out-badge")) > 0:
                sold = 1

            url = post.find_element_by_css_selector("a").get_attribute("href")
            se = pandas.Series([title, price, sold,url],['title','price','sold','url'])
            df = df.append(se, ignore_index=True)

        #5-1-4

        page+=1

        btn = browser.find_element_by_css_selector("li.pager-next .pager-cell:nth-child(1) a").get_attribute("href")
        print("next url:{}".format(btn))
        browser.get(btn)
        print("Moving to next page......")

    #5-2

    else:
        print("no pager exist anymore")
        break

Jupiter Notebookでデータの処理

検索結果はたぶんに無駄なものを含んでいます。そこで、Jupyter Notebookを使い、不必要なデータを処理します。まず、ルンバという言葉を含んでいないものを削除します。

import pandas as pd

df = pd.read_csv("ルンバ.csv")
df.head()
len(df) #1200件ほどになった

df = df[df["title"].str.contains("ルンバ")==True]
df.head(40) #1000件ほどになった



また、クリーナーなどの付属品や、故障品、ジャンク品も出てくるので、これも削除します。

import pandas as pd

df = df[df.title.str.contains("クリーナー") == False]
len(df)


df = df[df.title.str.contains("部品") == False]
len(df)

df = df[df.title.str.contains("ルンバ様") == False]
len(df)

df = df[df.title.str.contains("デュアルバーチャル") == False]
len(df)

df = df[df.title.str.contains("ジャンク") == False]
len(df)

df = df[df.title.str.contains("ホームベース") == False]
len(df)

df = df[df.title.str.contains("ルンバタイム") == False]
len(df)

df = df[df.title.str.contains("専用") == False]
len(df)

df = df[df.title.str.contains("チョッパー") == False]
len(df)

df = df[df.title.str.contains("故障") == False]
len(df)

df = df[df.title.str.contains("スタンド") == False]
len(df)

最終的に、770件ほどにまで絞り込めました。あとは目視で明らかにおかしいデータを除外しました。これをCSVにエクスポートします。


Tableauでデータ分析

tableauという無料のBIツールを使い、ヒストグラムを生成しました。

結果


ということで、分析結果を公表したいと思います。こちらがTableauで作成したヒストグラムです。セルの上が、割合で、下が個数ですね。左に山がきれいによっているヒストグラムなのです。ヒストグラムを見るだけでも、以下のことがわかります。

  • 価格が上がるにつれて、購入率が低下する
  • 価格は1-2万円のものが多い




(1) ヒトはルンバにいくらまで払うか⇒10万円くらいまで


ヒストグラムを見てもわかりますが、マックス10万円くらいでした。割と新品の売値と同じくらいでも買う人がいるんですね。逆に最安値は5000円くらいでした。


最頻値は1-2万円くらいでした。新品がだいたい5-6万円するので、かなりお得ですね。


ちなみにここにはありませんが、ジャンク品だったら3000円ぐらいで買えるようなので、割と新しいルンバをジャンクで買って、2-3万円で売るなんてのもいいかもしれません。


(2) いくらルンバの値段を上げると、いくら購入率が落ちるのか ⇒ 価格を上げると、購入確率は減少する


こちらが、ヒストグラムの横軸の購入率の割合をテーブルにしたものです。十分なサンプルデータが存在しなかったので、7万円以降は少し割合がおかしくなってしまっていますが、おおむね5万円ぐらいまでは、購入確率が低下しているように思われます。


価格(円) 購入率 購入数
10000 82.64 100
20000 75.72 184
30000 69.44 125
40000 66.67 84
50000 57.69 30
60000 50 15
70000 83 8
80000 100 5
90000 33 2
100000 62 5
こちらを散布図に落とし込んでみました。(Libre Officeで作っているので、少しグラフが見にくいですが)購入数が値段が高くなっていくごとに少なくなってくるので、割合でみるとめちゃくちゃなことになっています



6万円台にまで値段を上げると、購入確率が45%程度となり、逆に2万円台にすると、80%くらいの割合で購入してくれるようです


購入率と、値段で考えると、3万円くらいがちょうどよさそうですね。そこから上げすぎると、買ってくれなくなってきそうです。


もう少しデータがちゃんとしていれば、単回帰分析しても面白そうです。しかし、データセットに問題があるので、散布図にだけにとどめておきます。


考えたこと


出品者に、この値段を入力すると購入確率を洗い出してくれるような機能があったら、非常に面白そうですね。例えば、6万円で売ると、過去の実績から購入確率は45%程度ですみたいな。将来的には画像検索したら、その商品の売値価格が出てきたりとか。いろいろできそうです。

注目の投稿

 PythonのTweepyを利用して、Twitter APIを利用している。 その中で、ハマったポイントをメモしておく。 まず、Searchに関して。 Twitter検索は、クライアントアプリ側では、全期間の検索が可能になっている。 一方で、APIを利用する際は、過去1週間しか...