ローカルビジネスのオーナーに「緊急配管工」でのランキングを尋ねると、たいていは1つの数字を答える。多くの場合、それは自分のオフィスから検索したときに見た数字だ。その数字自体は本物だが、それは地図上の一点を表しているにすぎない。Googleはローカル検索結果を検索者からの距離によって部分的にランク付けしているため、同じ店舗が自分の通りでは1位でも、1マイル離れると8位になり、町の反対側では完全に圏外になることもある。
ジオグリッドはこの問題を、複数の地点から同時にランキングを測定することで解決する。ビジネスがサービスを提供する地域に座標のグリッドを敷き、各地点から同じマップ検索を実行し、ビジネスがどこに表示されるかを記録する。結果として得られるのは単一のランクではなくランキングの地図であり、ビジネスがどこで勝ち、どこで消えるかが正確にわかる。
このガイドでは、グリッドの設計方法、実行するコード、チームがそこから報告する3つの指標の読み方、そしてグリッドを誤解を招くものにしてしまう間違いについて解説する。
なぜ1つのランクでは不十分なのか
Googleの公式ガイダンスでは、ローカル検索結果の背後にある3つの要因を挙げている。関連性(リスティングが検索内容にどれだけ一致しているか)、距離(検索者または検索内で指定された場所から結果がどれだけ離れているか)、知名度(レビュー、リンク、その他のシグナルから見たビジネスの知られ具合)だ。関連性と知名度は1ブロック移動したくらいではほとんど変わらない。距離は一歩ごとに変化する。
これにより、ローカルランキングは数字ではなく曲面になる。プロフィールの強い歯科医院は地区全体で上位3位を維持できるかもしれないが、新しい競合は自分の店の近くでしかランクインしないかもしれない。オフィスからの単発チェックでは、この両方の事実が見えなくなる。都市レベルのチェックも同様だ。「シカゴで」の結果を尋ねることは、ローカルビジネスが気にするまさにその変動を平均化して消してしまう。
場所が検索結果をより広く左右する仕組みについて初めての場合は、ローカルSEOのための地域ターゲティングSERP APIに関するガイドで、国レベルから座標レベルまでの粒度の段階を解説している。ジオグリッドはその最後の段に位置する。
グリッドの設計
グリッドには3つの設定があり、それぞれがマップから読み取れる情報を変える。
| 設定 | 制御する内容 | 妥当な初期値 |
|---|---|---|
| サイズ | 一辺あたりの地点数(7は7 x 7 = 49地点を意味する) | 近隣ビジネスなら7、サービスエリアなら9から13 |
| 間隔 | 隣接する地点間の距離 | 密集した都市では0.5から1 km、郊外や地方では2から5 km |
| ズーム | 検索を行うマップのズームレベル | 街レベルなら15、より広い範囲なら13 |
3つのルールがグリッドの信頼性を保つ。
- 奇数サイズを使い、ビジネスを中心に置く。 これにより中央の地点が住所の位置に一致し、マップは常に周囲全体と並んで最良のケースを示すことになる。
- 間隔を実際の検索方法に合わせる。 コーヒーショップは徒歩圏内で競合するが、屋根工事業者は郡全体で競合する。グリッドが密すぎると緑一色になり、広すぎると空のセルばかりになる。
- 実行間ですべての設定を固定する。 サイズ、間隔、ズーム、キーワード、言語、デバイスは同一でなければならず、そうでなければ2つのマップを比較することはできない。
コード
以下のモジュールはグリッドを構築し、各地点ごとに1つのマップ検索を準備し、各レスポンスでビジネスを見つけ、結果をまとめる。標準ライブラリのみを使用している。
import math
EARTH_RADIUS_KM = 6371.0088
NOT_FOUND = 21 # one past the 20 places a Maps search returns
def grid_points(lat, lng, size=7, spacing_km=1.0):
"""A size x size grid centred on (lat, lng), points spacing_km apart."""
if size % 2 == 0:
raise ValueError("size must be odd so the business sits on the centre point")
half = size // 2
points = []
for row in range(size):
for col in range(size):
north_km = (half - row) * spacing_km
east_km = (col - half) * spacing_km
p_lat = lat + math.degrees(north_km / EARTH_RADIUS_KM)
p_lng = lng + math.degrees(east_km / (EARTH_RADIUS_KM * math.cos(math.radians(p_lat))))
points.append({"row": row, "col": col, "lat": round(p_lat, 6), "lng": round(p_lng, 6)})
return points
def maps_params(keyword, lat, lng, zoom=15, api_key="YOUR_API_KEY"):
"""Query parameters for one Google Maps search seen from (lat, lng)."""
return {
"engine": "google_maps",
"api_key": api_key,
"q": keyword,
"type": "search",
"ll": f"@{lat},{lng},{zoom}z",
"hl": "en",
}
def rank_of(response, place_id):
"""Position of the business in one response, or None when it is not listed."""
for place in response.get("places_results", []):
if place.get("place_id") == place_id:
return place.get("position")
return None
def summarise(ranks, top=3):
"""ranks: one rank (or None) per grid point."""
found = [r for r in ranks if r is not None]
return {
"points": len(ranks),
"found": len(found),
"arp": round(sum(found) / len(found), 2) if found else None,
"atrp": round(sum(r if r is not None else NOT_FOUND for r in ranks) / len(ranks), 2),
"solv": round(sum(1 for r in found if r <= top) / len(ranks), 3),
}
def render(points, ranks, size):
"""The grid as text, north at the top: rank per point, '--' when not found."""
cells = {(p["row"], p["col"]): r for p, r in zip(points, ranks)}
return "\n".join(
" ".join(f"{cells[(row, col)]:>2}" if cells[(row, col)] is not None else "--" for col in range(size))
for row in range(size)
)
grid_pointsは球面上でキロメートルを度数に変換するため、赤道から離れるにつれて引き伸ばされることなく、どの緯度でも間隔が均等に保たれる。地点0は北西の角にあり、行は北から南へ進む。
1つのビジネスとキーワードについて実行するのは短いループだ。各地点はShifterのSERP APIを通じた1つのGoogleマップ検索となり、座標はllパラメータで渡されるため、その地点から検索が行われる。
import json
import os
import time
import requests
from geogrid import grid_points, maps_params, rank_of, render, summarise
API = "https://serp.shifter.io/v1"
KEYWORD = "emergency plumber"
PLACE_ID = "ChIJ_YOUR_PLACE_ID" # the business you are tracking
CENTER = (41.8781, -87.6298) # its address
SIZE, SPACING_KM = 7, 1.0
points = grid_points(*CENTER, size=SIZE, spacing_km=SPACING_KM)
ranks = []
for p in points:
params = maps_params(KEYWORD, p["lat"], p["lng"], api_key=os.environ["SERP_API_KEY"])
response = requests.get(API, params=params, timeout=60).json()
ranks.append(rank_of(response, PLACE_ID))
time.sleep(0.2)
print(render(points, ranks, SIZE))
print(json.dumps(summarise(ranks)))
正しいリスティングに一致させるには、名前ではなくビジネスのplace_idを使う。名前は重複することがあり、フランチャイズは名前を共有している。place_idは、そのビジネスを含む任意のマップレスポンスの中で見つけることができる。
このモジュールを、中心地点からの距離によってビジネスをランク付けし、東側がやや弱いという設定の合成レスポンダーに対してテストした。出力は次のようになる(数字は合成テストによるもので、実在のビジネスのものではない)。
12 10 9 8 11 12 14
10 8 6 6 8 10 12
9 6 4 3 6 8 11
8 6 3 1 5 8 10
9 6 4 3 6 8 11
10 8 6 6 8 10 12
12 10 9 8 11 12 14
{"points": 49, "found": 49, "arp": 8.31, "atrp": 8.31, "solv": 0.082}
このパターンは実際のグリッドが示すものと同じだ。住所の位置では強く、距離とともに弱まり、方向によってムラがある。
マップの読み方: 3つの指標
チームはグリッドを3つの数字でまとめ、それぞれが異なる問いに答える。
| 指標 | 計算方法 | わかること |
|---|---|---|
| ARP(平均ランク位置) | ビジネスが表示された地点における平均ランク | 表示された場合にどれだけうまくランクされているか |
| ATRP(平均総合ランク位置) | 全地点における平均ランク、「見つからず」を21としてカウント | 地域全体にわたる全体的な可視性 |
| SoLV(ローカルボイスシェア) | ビジネスがマップパックで上位3位に入る地点の割合 | マップパック内でそのマップのどれだけを占有しているか |
これらは併せて読む必要がある。5地点でしか見つからず、そのすべてで上位3位に入っているビジネスは、優れたARPと乏しいATRPを持つことになる。マップはその理由を示している。小さなエリアを支配しているが、それ以外の場所では姿を見せていないのだ。これは関連性の問題ではなく距離の問題を示しており、ページ上の改善を加えてもそれは解決しない。
SoLVはマップパック(通常のリスティングの上にGoogleが表示する3つのローカル結果)に依存するビジネスにとって最も重要だ。4位から20位までの地位はマップの完全なリスト上に存在するが、そこまでスクロールする検索者ははるかに少ない。
意思決定にグリッドを使う
一回限りのグリッドは診断にすぎない。価値があるのは繰り返し行うグリッドだ。
- 同じグリッドで競合と比較する。 中心地点であなたを上回っている2、3のビジネスについて、同じ地点でグリッドを実行する。彼らのマップは、競合が知名度(広く均一なエリア)で勝っている一方で、あなたが近接性で勝っていることをよく示す。
- 行動後の変化を測定する。 新しいレビュー、修正されたカテゴリ、修正された住所は、数週間後により広い緑のエリアとして現れるはずだ。固定されたグリッドがなければ、改善をノイズと区別することはできない。
- 端を追跡する。 ビジネスの強いエリアの境界にあるセルは、どちらの方向にも最初に動く。それらを監視することで、中心を見るよりも早い警告が得られる。
- サービスエリア型ビジネスは注意深く確認する。 住所を非公開にして顧客のもとへ出向くビジネスは、実店舗とは大きく異なるランキングを示すことが多い。より広いグリッドは、顧客がいる場所に表示されているかどうかを示す。
ほとんどのビジネスには週次の実行で十分だ。ローカルランキングは日次で変動するが、固定設定での週次実行はすべての変動に対して課金されることなくトレンドを示す。通常のオーガニックランキングも追跡している場合は、SERPの変動性を検知するガイドで、実際の変動と日々のノイズを見分ける方法を解説している。
グリッドを誤解を招くものにしてしまう間違い
- キーワードに都市名を含めて検索する。 「配管工 シカゴ」はGoogleにどこを見るべきかを伝えてしまい、その地点の位置情報を上書きしてしまう。近隣の検索者が使うような、普通のサービス用語を使うこと。
- 実行間でデバイスや言語を混在させる。 モバイルとデスクトップのマップ結果は異なる。1つを選んで固定すること。
- 「見つからず」を20位のランクとして扱う。 存在しないビジネスは20位のビジネスよりも悪い。ATRPが21として行っているように、これは別途カウントし、見つかった地点の数を報告すること。
- 水面や空き地の上でグリッドを実行する。 湖や森林の中の地点は、顧客が決して検索しないセルを追加するだけだ。中心をずらすか、平均化する前にそれらのセルを取り除くこと。
- 1回の実行を真実として読む。 同一の検索でも結果はわずかに変動する。1つのセルの色が変わったことではなく、複数回の実行にわたって保たれるパターンに基づいて行動すること。
クリーンで一貫したランク測定(パーソナライズ、デバイス、時間)についての全体像は、正確なキーワードランキングの測定方法を参照してほしい。
コストと規模
グリッドのコストは単純に、地点数×キーワード数×実行回数だ。7 x 7のグリッドは1キーワードあたり49回の検索になる。週次で追跡する10個のキーワードなら週490回、月あたり約2,100回の検索になる。同じ10個のキーワードで13 x 13のグリッドなら、週1,690回の検索になる。
この計算が、グリッドのサイズを慎重に選ぶべき主な理由だ。顧客をもたらすキーワードについて7 x 7のグリッドから始め、マップの端がまだ変化しているものだけを広げていく。
よくある質問
ジオグリッドにはいくつの地点が必要か?
近隣地域にサービスを提供するビジネスであれば、7 x 7のグリッド(49地点)で通常は十分だ。都市全体や郡全体にサービスを提供するビジネスは、より広い間隔が必要で、時には9 x 9から13 x 13のグリッドが、可視性がどこで終わるかを見るために必要になる。
ジオグリッドはZIPコード別のランキングチェックと同じか?
いいえ。ZIPコードチェックはある地域に対して1回の検索を実行し、1つのランクを返す。ジオグリッドは各座標から検索を実行するため、エリア内でランキングがどう変化するかを示す。これは距離ベースのローカル検索結果がまさに依存しているものだ。
なぜオフィスでは1位なのに、町の反対側ではランクインしないのか?
距離はGoogleのローカル検索結果における主要な要因の一つだ。住所の近くでは距離が有利に働く。離れた場所では、検索者により近い、あるいはより知名度の高い競合が上位の地位を占める。
ジオグリッドはどのくらいの頻度で実行すべきか?
ほとんどのビジネスには週次で十分だ。新しいレビューやビジネスカテゴリの修正といった大きな変更の数週間後に追加のグリッドを実行し、強いエリアが拡大したかどうかを確認すること。
結論
ローカルランキングは数字ではなく地図だ。ジオグリッドは複数の地点から同じ検索を行うことでその地図を測定し、3つの指標(ARP、ATRP、ローカルボイスシェア)がそれをまとめる。グリッドをビジネスの中心に置き、実際の市場規模に合わせてサイズを決め、実行間で固定しておけば、それはローカルビジネスがどこで可視化されているか、どこでそうでないか、そしてそれが変化しているかどうかを見るための最も直接的な方法になる。