価格ドットコムをスクレイピングしてゲーミングPCを調査してみた(後編)

スポンサーリンク
データ解析
スポンサーリンク

価格ドットコムのゲーミングPCの製品情報をWebスクレイピングで取得して解析をしようとしています。前編では、Webデータを取得して、PandasのDataFrameとして保存するところまでやりました。後半は、データを解析し、ゲーミングPCの価格構造を解析してみます。Webスクレイピングの方法は前編↓をご覧ください。

この記事でやっていること

ここではスクレイピングを使って抽出した価格ドットコムのゲーミングPCのデータの解析をしています。具体的にはPythonを使って次のようなことをやっています。

  • ヒストグラムでデータの概要確認
  • データの前処理
  • 仕様で価格の補正
  • 補正した価格で平均算出
  • 箱ひげ図の作成

一連の作業を通じでデータ処理の方法を学習していきます。元データの抽出方法については前編をご覧ください。なお、今回の解析は2022年8月12日にスクレイピングしたデータを使っています。また、Pythonはインタラクティブな実行環境のJupyter Labを使って進めています。

価格の分布をみる

まず、データ全体の外観を確認してみます。価格の度数分布(ヒストグラム)を取ってみます。最初に使用するライブラリのインポートと前編でPandasのDataFrameを保存したpickleデータの読み出しをします。DataFrameをpickleで保存しておくと、呼び出してすぐに使うことができるので非常に便利です。

# ライブラリのインポート

import matplotlib.pyplot as plt
import pandas as pd
# データの読み出し
gamingPC = pd.read_pickle("gamingPC_data202208_1.pickle")

DataFrameには719製品のゲーミングPCのデータを入っています。まず、ヒストグラムでどのくらいの価格分布なのか見てみます。価格はgamingPC[‘price’]のカラムにデータが入っていますので、それをmatplotlibのhistでヒストグラムを作ってみます。

plt.style.use('seaborn-bright')

fig, ax = plt.subplots(figsize=(7, 7))
ax.grid()
ax.set_xlim(0,1600000)
ax.hist(gamingPC['price'], bins=100, edgecolor='blue', color='yellow')

ax.set_xlabel('price')
ax.set_ylabel('hist')
ax.ticklabel_format(style='plain',axis='x')

plt.show()

ちなみに、画面に出力されたグラフを画像データとして保存するのは、”savefig”で行うことができます。matplotlibで画面に出力したあとで、以下のように入れると、画面に表示されたグラフ領域をそのまま画像に保存できます。

fig.savefig("hist1.png")

出力される画像はこんな感じです。

100万円くらいのところと、150万円くらいのところに1製品ずつありますが、おおむね20万円くらいのところを中心に価格帯を形成しているのが分かります。50万円以下のところをもう少し細かく見てみます。ヒストグラムの引数rangeでヒストグラムの範囲を選択できます。

fig, ax = plt.subplots(figsize=(7, 7))
ax.grid()
ax.set_xlim(0,500000)
ax.hist(gamingPC['price'], bins=50, range=(0,500000), edgecolor='blue', color='yellow')

ax.set_xlabel('price')
ax.set_ylabel('hist')
ax.ticklabel_format(style='plain',axis='x')

plt.show()

また、積み上げグラフでも見てみましょう。histの引数で「cumulative = True」を指定することで累積表示ができます。もし、全体を1に規格化して割合を表示したいなら、引数に「density = True」を指定します。ここでは、縦軸は件数で表示することにします。

fig, ax = plt.subplots(figsize=(7, 7))
ax.grid()
ax.set_xlim(0,500000)
ax.hist(gamingPC['price'], bins=50, range=(0,500000), edgecolor='blue', color='yellow', cumulative = True)

ax.set_xlabel('price')
ax.set_ylabel('hist')
ax.ticklabel_format(style='plain',axis='x')

plt.show()

matplotlibのヒストグラムの詳しい使い方は公式ドキュメントを参照してください。

matplotlib.pyplot.hist(公式ドキュメント)

PCの価格決定の重要な要素について

ゲーミングPCの価格ですが、使っているパーツの影響を受けます。特に、CPUやグラフィックカードの種類、メインメモリやストレージはPCの性能に大きな影響を与え、CPUとグラフィックカードは「ベンチマーク」という性能を測る指標によってランク付けがされています。(当然ですが、一般的に性能の高い部品は高価)価格ドットコムでは「CPUスコア」という指標でCPUの性能の目安を表してくれているので、解析には「CPUスコア」を使いたいと思います。ここではCPUとグラフィックカードに焦点を当てて解析します。

グラフィックカードの要素をvalue_counts()で確認してみました。グラフィックカードの中で、FPSゲームも十分楽しめるGeForce RTX 3060や3060Ti、3050などが上位を占めています。

gamingPC['graphics_board'].value_counts()
GeForce RTX 3060          88
GeForce RTX 3060Ti        63
GeForce RTX 3050          60
GeForce RTX 3080          50
GeForce RTX 3070Ti        50
GeForce GTX 1660 SUPER    48
GeForce RTX 3080Ti        41
GeForce RTX 3070          37
GeForce RTX 3060 LHR      33
Radeon RX 6700 XT         28
GeForce RTX 3090          26
GeForce RTX 3080 LHR      19
GeForce RTX 3090Ti        18
Intel UHD Graphics 770    17
GeForce RTX 3060Ti LHR    17
GeForce RTX 3070 LHR      16
Radeon Graphics           14
Radeon RX 6600             9
GeForce GT 710             8
Radeon RX 6900 XT          8
Radeon RX 6600 XT          7
GeForce GTX 1660Ti         7
Intel UHD Graphics 730     6
GeForce RTX 3050 LHR       6
GeForce GTX 1650           5
GeForce RTX 3080Ti LHR     4
Radeon RX 6650 XT          4
Radeon RX 6400             4
Intel UHD Graphics 750     3
GeForce GTX 1650(G6)       3
GeForce RTX 2060           3
GeForce RTX 3070Ti LHR     3
Radeon RX 6500 XT          2
Radeon RX 6800 XT          2
GeForce GTX 1650 SUPER     2
GeForce RTX 3090 LHR       2
GeForce GTX 1060           1
GeForce GTX 1050Ti         1
Radeon RX 5500 XT          1
GeForce RTX 2080 SUPER     1
GeForce RTX 2080Ti         1
Radeon Pro W6800           1
Name: graphics_board, dtype: int64

その中で、”LHR”がついているグラフィックボードがありましたが、調べてみると、グラフィックボード側でマイニング用に使えないようになっている仕様のようです。近年、グラフィックボードがマイニング用に大量に使われていて、本来のゲーム用が不足するという事態が起こっているようでそれに対応したグラフィックボードメーカー側の対応のようです。ゲームをする上では、関係ないので、”LHR”なしのものと区別する必要はないので、データからはこの表記を削除することにします。グラフィックボードの列に手を加える前に、オリジナルを残しておくために、列をコピーして新しく加工用の列を追加して、そこを加工します。文字列の置換はstr.replaceで行います。” LHR”を空文字列”” に置換します。これにより、42種類あったグラフィックボード種類を36種類に減らすことができました。

gamingPC['graphics_board_r'] = gamingPC['graphics_board']
gamingPC['graphics_board_r'] = gamingPC['graphics_board_r'].str.replace(' LHR','')

それでは、scatterでグラフを表示してみます。グラフィックボード別にマーカーの色と種類を変えています。defaultのカラーパレットでは10色周期で色が変わっていくので、ここでは前半の10種類を■、後半の10種類を×で合計20個のグラフィックボードのデータを色を変えてプロットしていきます。マーカーの種類はmatplotlibの公式に他にもいろいろ載っています。

matplotlib.markers(公式ドキュメント)

グラフィックボードのユニーク要素を多い順番にfor文に渡していき、Pandasの抽出で、そのグラフィックボードの名称に一致するものを抜き出していき、ブロットします。横軸にはCPUスコアを縦軸には価格でプロットしています。

plt.style.use('default')

fig, ax = plt.subplots(figsize=(10, 10))
ax.grid()


ax.set_ylim(0,1500000)
for gra_bo in gamingPC['graphics_board_r'].value_counts()[:10].index:
    ax.scatter(gamingPC[gamingPC['graphics_board_r']==gra_bo]['CPU_score'], 
               gamingPC[gamingPC['graphics_board_r']==gra_bo]['price'], marker='o', alpha = 0.7)
for gra_bo in gamingPC['graphics_board_r'].value_counts()[10:20].index:
    ax.scatter(gamingPC[gamingPC['graphics_board_r']==gra_bo]['CPU_score'], 
               gamingPC[gamingPC['graphics_board_r']==gra_bo]['price'], marker='x')

    
ax.legend(gamingPC['graphics_board_r'].value_counts().index, fontsize=10)
ax.set_xlabel('CPU_score')
ax.set_ylabel('price')
ax.ticklabel_format(style='plain',axis='y')

plt.show()

メモリ容量の補正

PCはメモリ容量が大きいほど、価格がなります。BTO(Build to Order)のPCは一般的にユーザーの希望でメモリを増強することができます。メーカーにも寄りますが、おおむね8GBを16GBに増やすには6000円くらい、16GBを32GBに増やすには17500円くらい、32GBを64GBに増やすには33000くらい掛かります。勿論、使っているマザーボードの種類によって、積めるメモリの大きさや種類は変わってきますが、ここでは簡単にするためにそのような制限がないとしたときの、32MB相当のメモリを搭載した場合の換算価格を考えています。今回販売されているPCのメモリの内訳をみると、このようになります。

gamingPC['memory'].value_counts()
16GB     491
32GB     213
64GB       9
8GB        5
128GB      1
Name: memory, dtype: int64

ゲームをストレスなくやろうと思ったら、32GBくらいが主流だと思います。16GBで販売されているPCも大抵はカスタマイズで32GBに増強できるものが多いと思いますので、その人の目的に合わせてメモリを調節できるように選びやすいように小さめのメモリを標準仕様で入れているものと思います。ここでは、(私の)本命の32GBの換算した補正値を算出します。価格はマウスコンピュータのホームページからカスタマイズした価格を取って、それを補正値として使います。

メモリ容量補正金額
8GB+\21,890
16GB+\17,490
32GB±0
64GB-\32,800
128GB-\128,590
# 32GBメモリとの価格差補正
memory_converted_price = {'8GB': 21890, '16GB': 17490, '32GB': 0, 
                          '64GB': -32800, '128GB': -128590}

ストレージ容量の補正

引き続きストレージを見ます。こちらの方はSSDとHDDの容量がいろいろあり、全部で30種類以上あり、ちょっと大変です。

gamingPC['storage'].value_counts()
M.2 SSD:1TB             309
M.2 SSD:500GB           135
M.2 SSD:512GB           103
SSD:1TB                  39
HDD:2TBM.2 SSD:512GB     24
SSD:500GB                20
HDD:2TBM.2 SSD:1TB       14
HDD:4TBM.2 SSD:1TB       10
SSD:512GB                 9
HDD:1TBM.2 SSD:1TB        8
HDD:1TBM.2 SSD:512GB      8
M.2 SSD:2TB               7
M.2 SSD:500GB+1TB         3
HDD:1TBSSD:512GB          2
HDD:1TBM.2 SSD:500GB      2
HDD:2TBM.2 SSD:960GB      2
M.2 SSD:512GB+1TB         2
HDD:1TBM.2 SSD:256GB      2
M.2 SSD:1TB+2TB           2
HDD:4TBM.2 SSD:512GB      2
HDD:2TBM.2 SSD:500GB      2
HDD:2TBM.2 SSD:256GB      2
M.2 SSD:256GB             2
HDD:1TBSSD:128GB          1
SSD:2TB                   1
SSD:4TB                   1
SSD:1TBM.2 SSD:1TB        1
HDD:8TBM.2 SSD:1TB        1
SSD:250GB                 1
HDD:2TBSSD:500GB          1
SSD:256GB                 1
HDD:1TBSSD:1TB            1
HDD:4TBM.2 SSD:2TB        1
Name: storage, dtype: int64

ここは正規表現を使って、SSDでHDDの値を抽出してみます。SSDですが、M.2 SSDとSSDがありますが、その違いはここでは考えないでおきます。小型のM.2 SSDの方が単価は高いと思いますが、そのあたりはマザーボードの大きさにも関わってくるし、計算を簡略化するために無視します。ここでは正規表現で、”SSD”と”B”で挟まれた領域の文字列を取得したら良さそうです。

gamingPC['SSD'] = gamingPC['storage'].str.extract('(SSD:[0-9]+[TG]B)')
gamingPC['SSD'] = gamingPC['SSD'].str.replace('SSD:', '')
gamingPC['SSD'].value_counts()
1TB      385
500GB    163
512GB    150
2TB        9
256GB      7
960GB      2
250GB      1
4TB        1
128GB      1
Name: SSD, dtype: int64

ストレージは1TBを基準に換算します。細かい容量の違いは無視してよいので、512GBは500GB、250GBは256GBに、960GBは1Tと同等と扱います。また、128GBのものは1件ですが、これも便宜上256GBと同等と扱います。こちらもマウスコンピュータのカスタマイズの金額から1Tに換算する補正値を持ってきます。4TBのものはマウスコンピュータで取り扱いがなかったので、単体の実勢価格を入れています。

SSD容量補正金額
128GB+\20,790
250GB
256GB
+\20,790
500GB
512GB
+\16,390
960GB
1TB
±0
2TB-\26,290
4TB-\90,000
# 1TB SSDとの価格差補正
SSD_converted_price = {'128GB': 20790, '250GB': 20790, '256GB': 20790, '500GB': 16390, 
                       '512GB': 16390, '960GB': 0, '1TB': 0, '2TB': -26290, '4TB': -90000}

HHDも同じように抽出します。HDD未搭載のモデルには”0″をいれます。

gamingPC['HDD'] = gamingPC['storage'].str.extract('(HDD:[0-9]+[TG]B)')
gamingPC['HDD'] = gamingPC['HDD'].str.replace('HDD:', '')
gamingPC['HDD'] = gamingPC['HDD'].fillna('0')
gamingPC['HDD'].value_counts()
0      636
2TB     45
1TB     24
4TB     13
8TB      1
Name: HDD, dtype: int64

HDDは”HDDなし”を基準として考えて、補正金額を出します。

HDD容量補正金額
0±0
1TB-\7,590
2TB-\9,790
4TB-\13,090
8TB-\27,390
# HDDなしとの価格差補正
HDD_converted_price = {'1TB': -7590, '2TB': -9790, '4TB': -13090, '8TB': -27390, '0': 0}

補正金額の算出

それでは、補正金額の列を新たに作成してみます。上記で作成した辞書型の補正テーブルを使って、Pandasの列のデータを参照して、補正金額を計算します。各列からの金額の換算はapplyメソッドを使って行うことにします。メモリ、SSD、HDDの補正金額を足し合わせて、”converted_price”の列に入れます。これにより、この金額を元の金額と足し合わせることで、「メモリ:32GB、SSD:1TB、HDDなし」相当の換算価格を算出できます。その値を”calculated_price”の列に入れます。

gamingPC['converted_price'] = gamingPC['memory'].apply(lambda x: memory_converted_price[x]) \
                                + gamingPC['SSD'].apply(lambda x: SSD_converted_price[x]) \
                                + gamingPC['HDD'].apply(lambda x: HDD_converted_price[x])
gamingPC['calculated_price'] = gamingPC['price'] + gamingPC['converted_price']

新しく作成した”calculated_price”で再びグラフィックボードごとのCPU_scoreとの関係をscatterで表してみます。

plt.style.use('default')

fig, ax = plt.subplots(figsize=(10, 10))
ax.grid()


ax.set_ylim(0,1500000)
for gra_bo in gamingPC['graphics_board_r'].value_counts()[:10].index:
    ax.scatter(gamingPC[gamingPC['graphics_board_r']==gra_bo]['CPU_score'], 
               gamingPC[gamingPC['graphics_board_r']==gra_bo]['calculated_price'], marker='o', alpha = 0.7)
for gra_bo in gamingPC['graphics_board_r'].value_counts()[10:20].index:
    ax.scatter(gamingPC[gamingPC['graphics_board_r']==gra_bo]['CPU_score'], 
               gamingPC[gamingPC['graphics_board_r']==gra_bo]['calculated_price'], marker='x')

    
ax.legend(gamingPC['graphics_board_r'].value_counts().index, fontsize=10)
ax.set_xlabel('CPU_score')
ax.set_ylabel('calculated_price')
ax.ticklabel_format(style='plain',axis='y')

plt.show()
補正金額を反映した価格

グラフィックボードごとの平均価格

今回、求めた「メモリ:32GB、SSD:1TB、HDDなし」相当の換算価格から、グラフィックボードごとのゲーミングPCの平均価格を計算してみます。ここでは、9個以上データのあるグラフィックボードの平均価格を計算します。ここでは、極端に値段が違うPCを平均の計算から除外するために四分位範囲を使います。四分位範囲の定義や計算方法は参考ページの説明を参照してください。ここでは、それらの四分位範囲に使うパラメーターを含む新しくグラフィックボードごとのデータ数や最大、最小、平均などをまとめたDataFrameを作ることにします。
ベースにはvalue_countsで生成されるSeriesをDataFrameに変換して使います。今回の計算は9個以上のデータがあるものに限定しますので、DataFrameの抽出を行って、絞り込みをします。そうして処理したDataFrameを下に示します。

graphics_board_summery = pd.DataFrame(gamingPC['graphics_board_r'].value_counts())
graphics_board_summery.columns = ['counts']
graphics_board_summery = graphics_board_summery[graphics_board_summery['counts'] >= 9]
print(graphics_board_summery)
                        counts
GeForce RTX 3060           121
GeForce RTX 3060Ti          80
GeForce RTX 3080            69
GeForce RTX 3050            66
GeForce RTX 3070            53
GeForce RTX 3070Ti          53
GeForce GTX 1660 SUPER      48
GeForce RTX 3080Ti          45
GeForce RTX 3090            28
Radeon RX 6700 XT           28
GeForce RTX 3090Ti          18
Intel UHD Graphics 770      17
Radeon Graphics             14
Radeon RX 6600               9

引き続き、計算に使用する列を追加します。各値は以下の通りです。

graphics_board_summery['low'] = 0 # 最小金額
graphics_board_summery['high'] = 0 # 最大金額
graphics_board_summery['average'] = 0 # 平均金額(ただし、外れ値は除いた平均) 
graphics_board_summery['q25'] = 0 # 第1四分位の値(25%)
graphics_board_summery['q50'] = 0 # 第2四分位の値;中央値(50%)
graphics_board_summery['q75'] = 0 # 第3四分位の値(75%)
graphics_board_summery['low_limit'] = 0 # 四分位の下限
graphics_board_summery['high_limit'] = 0 # 四分位の上限
graphics_board_summery['outlier'] = 0  # 外れ値の数

四分位範囲や平均を計算します。なかなか良い式が思いつかなかったので、長ったらしい式になってしまいました。やっている計算は、元のgamingPCのDataFrameから条件絞り込みで抽出したDataFrameに対して、四分位の値や外れ値の数、外れ値を除いた平均などを求めています。

for i in range(len(graphics_board_summery)):
    low, q25, q50, q75, high = gamingPC['calculated_price']\
    [gamingPC['graphics_board_r'] == graphics_board_summery.index[i]].quantile(q = [0, 0.25, 0.5, 0.75, 1])
    low_limit = q25 - (q75 - q25)*1.5
    high_limit = q75 + (q75 - q25)*1.5
    graphics_board_summery['low'][i] = int(low)
    graphics_board_summery['high'][i] = int(high)
    graphics_board_summery['q25'][i] = int(q25)
    graphics_board_summery['q50'][i] = int(q50)
    graphics_board_summery['q75'][i] = int(q75)
    graphics_board_summery['low_limit'][i] = int(low_limit)
    graphics_board_summery['high_limit'][i] = int(high_limit)
    
    graphics_board_summery['outlier'][i] = graphics_board_summery['counts'][i] - gamingPC['calculated_price']\
    [gamingPC['graphics_board_r'] == graphics_board_summery.index[i]][gamingPC['calculated_price'] >= low_limit]\
    [gamingPC['calculated_price'] <= high_limit].count()
    
    graphics_board_summery['average'][i] = int(gamingPC['calculated_price']\
                                               [gamingPC['graphics_board_r'] == graphics_board_summery.index[i]]\
                                               [gamingPC['calculated_price'] >= low_limit][gamingPC['calculated_price']\
                                                                                           <= high_limit].mean())

作成したDataFrameを確認します。

graphics_board_summery

うまく計算できましたので、Matplotlibで棒グラフで平均価格を図にしてみます。これを見ればだいたいの相場が分かります。

plt.style.use('default')

fig, ax = plt.subplots(figsize=(10, 10))
ax.grid(axis = 'x')
ax.set_xlabel('calculated price')

barh1 = ax.barh(graphics_board_summery.index, graphics_board_summery['average'])
ax.bar_label(barh1)

plt.title('Prices of Gaming PCs by Graphics Board: Calculated with Equivalent Options of 32 GB Memory and SDD 1 TB')
plt.show()

箱ひげ図を描く

ついでに箱ひげ図にも挑戦してみます。箱ひげ図のデータは各系列のデータをリストに入れて二次元の配列として作成します。今回は元のgamingPCのDataFrameから各グラフィックボードごとの価格データをリストとして抽出し、二次元配列を作ったうえで、タプルに変換しました。価格データは上で算出したメモリ32GB、SSD1TB仕様に換算した価格を使っています。

box_data = []
for i in range(len(graphics_board_summery)):
    box_data.append(list(gamingPC['calculated_price'][gamingPC['graphics_board_r'] == graphics_board_summery.index[i]]))
box_data = tuple(box_data)

箱ひげ図の作成方法は公式ドキュメントを参考にしました。”vert”の引数をFalseにすることで横方向の箱ひげ図になります。各項目の軸ラベルは”set_yticks”で数値軸をグラフィックボードの名称に変換しています。

matplotlib.pyplot.boxplot(公式ドキュメント)

plt.style.use('default')

fig, ax = plt.subplots(figsize=(10, 10))
ax.grid(axis = 'x')
ax.set_xlabel('calculated price')
ax.set_xlim(100000,800000)

ax.boxplot(box_data, vert = False)
ax.set_yticks(range(1, len(graphics_board_summery)+1), graphics_board_summery.index)

plt.show()

これで価格ドットコムでのゲーミングPCの価格を可視化できました。例えば、人気のグラフィックボードGeForce RTX 3060Tiの付属したPCであれば、メモリ32GB、SSD1TBの機種で、およそ20万円弱から30万円ちょっとの値段で売られていて、ボリュームゾーンとしては22万円から27万円くらいだと分かります。

最後に

Pythonを使うと本当にいろいろなことができます。今回の価格解析で自分の欲しいPCの目星をつけることができました。最近、グラフィックメモリも価格が下落基調だと聞いているので、うまいタイミングで買おうと思っています。

コメント