キーワードクラスタリング(キーワード クラスタリング)は、検索クエリを意味の近さでグループに束ね、1グループ=1記事という単位に対応づける作業です。主要な日本語の解説記事を確認した範囲では、扱いはツールの操作手順かAIへの指示文が中心で、データの取得仕様や日本語処理の挙動まで踏み込んだものは見当たりませんでした。実装に入ると、詰まるのはまさにその2点です。Search Consoleから取り出したデータが最初から欠けていること、そして日本語のキーワードをそのままベクトル化すると同義のクエリが「無関係」と判定されること。この記事では両方を実測値で確認しながら、クラスタを記事の施策に落とすところまでを扱います。
まとめ
キーワードクラスタリングの実装でつまずく箇所は、アルゴリズムの選定ではなく前工程に集中します。Search Console のデータには匿名化クエリが含まれ、クエリ名としては取り出せません。BigQuery一括エクスポートでは長さ0の文字列として行が入るため除外が要ります。クエリでフィルタすると合計値も変わるので、クラスタごとの表示回数は全体比ではなく相対比較に使ってください。取得件数は画面のエクスポートではなく Search Analytics API を使い、rowLimit の上限25,000と startRow のページングで取り切ります。
ベクトル化では、scikit-learn の TfidfVectorizer を既定設定のまま日本語に当ててはいけません。既定の token_pattern は空白や記号でしか語を切らず、日本語の連続した文字列は丸ごと1語になるため、「キーワード クラスタリング」と「キーワードクラスタリング やり方」のコサイン類似度は0.000になります。analyzer="char_wb" の文字N-gramに替えると0.623まで上がります。クラスタ数はシルエット係数の最大値で選ぶ方法が広く紹介されていますが、キーワード件数が少ないうちは乱数シード次第で係数が大きく振れ、単独の判断材料になりません。kは係数から選ぶのではなく、1クラスタあたりの件数から先に範囲を絞ってください。
キーワードクラスタリングの定義と、1記事1キーワードが成立しなくなる条件
クラスタリングは、データを類似性に基づいてグループへ分ける教師なし学習の総称です。手法そのものの体系(階層型と非階層型の違い、樹形図の読み方)はデンドログラムと階層的クラスタリングの解説で扱っているため、ここではSEOの文脈に限定します。
同義クエリの分散という、クラスタリングが解く問題
「キーワード クラスタリング」「キーワードクラスタリング」「キーワード クラスタリング とは」は、検索する人にとって同じ質問です。これを別々の記事で受けると、被リンクと内部リンクの評価が複数URLに割れ、どれも上位に届かなくなります。クラスタリングは、この分散を機械的に検出して1本にまとめる作業です。逆に言えば、扱うキーワードが数十件で、担当者が全部を目で見て分類できる規模なら、実装する価値はありません。
意味ベースとSERP重複ベース、2つの流派の使い分け
クラスタリングの基準には2系統あります。1つはキーワードの文字列や埋め込みベクトルの近さで束ねる意味ベース。もう1つは、実際の検索結果ページで上位URLがどれだけ重複するかで束ねるSERP重複ベースです。後者はGoogleが同一の意図と見なしているかを直接測るため精度は高いのですが、キーワード1件ごとに検索結果の取得が必要で、数千件規模では取得コストが実装の主要因になります。
手元にSearch Consoleのデータがあり、既に表示実績のあるクエリを整理したいのであれば意味ベースで足ります。新規に狙う市場をゼロから設計するなら、重要な候補だけSERP重複で検証する併用が現実的です。この記事は前者、つまり自社の実績データを束ねる側を扱います。
クラスタリングツールで足りる条件と、自前実装に切り替える境界
キーワードツールの多くはクラスタリング機能を備えており、生成AIにキーワード一覧を渡して分類させる方法も広く使われています。狙うキーワードを外部ツールの検索ボリュームから選び、分類結果をそのまま記事案にするだけなら、実装する必要はありません。
自前で組む理由は3つの場面に限られます。第一に、入力をSearch Consoleの自社実績にしたい場合。ツールは外部のボリューム推定を入力にするため、自社が実際に表示されているクエリとは母集団が違います。第二に、分類の基準を固定して毎月同じ条件で回したい場合。生成AIによる分類は実行ごとに結果が揺れ、前月との差分が取れません。第三に、クラスタを既存URLの順位データと結合したい場合です。この記事が扱う手順は、いずれもこの3条件に当てはまるときの選択肢です。
Search Consoleのデータを材料にするときに、最初から欠けているもの
クラスタリングの入力は、Search Console(サーチコンソール)のパフォーマンスデータです。ところがこのデータは、取り出した時点で完全ではありません。この節で扱う欠損は公式ヘルプに明記されているものですが、無視すると出来上がったクラスタが実態とずれます。
匿名化クエリの扱いが、取得経路によって変わる点
Googleはプライバシー保護のため、一部のクエリを匿名化します。ここで注意が要るのは、匿名化クエリの現れ方が取得経路ごとに違うことです。混同すると、書いたはずの前処理が空振りします。
BigQueryへの一括エクスポートを使う場合、公式ドキュメントは「匿名化されたクエリは、テーブルで長さが 0 の文字列として報告されます」と明記し、あわせて「長さがゼロのクエリ値を SQL クエリから除外することをおすすめします」と案内しています。対象は searchdata_site_impression と searchdata_url_impression の2テーブルです。この空文字行を除外せずにベクトル化へ流すと、内容のない行が1つのクラスタを形成します。
一方、この記事で使う Search Analytics API のリファレンスには、匿名化クエリに関する記述がありません。長さ0の文字列が返るという保証はないため、空文字を除外する前処理をAPI側の対策として当てにしないでください。APIで確実に言えるのは、匿名化されたクエリはクエリ名として取り出せない、ということだけです。
結果として、どちらの経路でもクエリ単位で足し上げた表示回数はサイト全体の数値に届きません。Search Console のヘルプも、クエリやURLでデータをフィルタすると、データの切り捨てと匿名化クエリの除外によってレポートの合計が変わる場合があると説明しています。クラスタごとの表示回数は全体に対する比率として扱わず、クラスタ間の相対比較にとどめてください。
取りこぼしはもう1段あります。実行回数が非常に少ないクエリや個人情報を含むクエリはトラッキングされない場合があること、内部的な制限により保存されるデータ行は上位のものに限られることが、いずれもヘルプに明記されています。ロングテールを取り切ったつもりでも、最も細いテールは最初から手元に届いていません。
画面の書き出しではなくAPI、rowLimit 25,000とstartRowのページング
Search Console の画面からのエクスポートでは、クラスタリングに必要な件数を取り切れません。Search Analytics API の searchanalytics.query を使います。公式リファレンスによれば rowLimit の有効範囲は1〜25,000で既定値は1,000、startRow は非負の値で既定0です。1回のリクエストで25,000行、それ以上は startRow を25,000ずつ進めて、返却行数が上限未満になった時点で打ち切ります。
# Search Analytics API から query 次元を全件取得する
# 認証スコープ: https://www.googleapis.com/auth/webmasters.readonly
from googleapiclient.discovery import build
from google.oauth2 import service_account
SITE = "sc-domain:example.com"
creds = service_account.Credentials.from_service_account_file(
"service_account.json",
scopes=["https://www.googleapis.com/auth/webmasters.readonly"],
)
service = build("searchconsole", "v1", credentials=creds)
ROW_LIMIT = 25000 # API の上限。既定は 1000 なので必ず明示する
rows, start = [], 0
while True:
body = {
"startDate": "2026-02-15",
"endDate": "2026-08-13",
"dimensions": ["query", "page"],
"dataState": "final", # 確定値のみ。速報を含めるなら "all"
"rowLimit": ROW_LIMIT,
"startRow": start,
}
res = service.searchanalytics().query(siteUrl=SITE, body=body).execute()
batch = res.get("rows", [])
rows.extend(batch)
if len(batch) < ROW_LIMIT:
break
start += ROW_LIMIT
# 空クエリ行の保険。BigQuery一括エクスポート併用時に効く
# (Search Analytics API 側は長さ0の文字列を返す保証がない)
rows = [r for r in rows if r["keys"][0]]
print(len(rows))
# 注: サービスアカウントは対象プロパティのユーザーとして
# 追加しておかないと 403 になります
dimensions に指定できるのは country、device、page、query、searchAppearance、date、hour です。ただし hour を使うには dataState に hourly_all を指定する必要があります。クラスタを既存URLに突き合わせる工程で page が必要になるため、query と page を同時に取っておくと後の作業が減ります。dimensionFilterGroups の演算子は contains、equals、notContains、notEquals、includingRegex、excludingRegex が使えますが、前述のとおりフィルタを当てると匿名化クエリが落ちるため、絞り込みはAPI側ではなく取得後のデータフレーム上で行ってください。
2〜3日の反映遅れと、dataStateの指定
Search Console のヘルプは「収集されたデータは通常、2〜3 日以内に利用可能になります」と説明しています。終了日を実行日当日に設定すると、直近数日が未確定のまま集計に混ざります。dataState には all、final、hourly_all があり、既定は final(確定値のみ)です。クラスタリングは数か月の集計を材料にするため final のまま、終了日を実行日の3日前に置くのが安全です。速報値を見たい場合だけ all を指定します。
日本語キーワードのベクトル化で、類似度が0になる理由
ここが実装で最も静かに失敗する箇所です。エラーは出ません。それらしいクラスタが返ってきます。しかし中身が壊れています。
既定のTfidfVectorizerが同義キーワードを無関係と判定する仕組み
scikit-learn の TfidfVectorizer は、既定の token_pattern が (?u)\b\w\w+\b です。この正規表現は英語のように空白で語が区切られる言語を前提としています。中黒や括弧、カンマでは切れるものの、日本語には単語境界がありません。結果として「空白や記号で切っただけの塊」が丸ごと1語になります。
影響を実際の数値で確認します。次のコードを scikit-learn 1.6.1 で実行すると、意味としては同一の2つのキーワードが、類似度0と判定されます。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
docs = ["キーワード クラスタリング", "キーワードクラスタリング やり方"]
for name, kw in [("既定のTfidfVectorizer", {}),
("char_wb 2-3gram", dict(analyzer="char_wb", ngram_range=(2, 3)))]:
X = TfidfVectorizer(**kw).fit_transform(docs)
print(f"{name}: cos = {cosine_similarity(X[0], X[1])[0][0]:.3f} vocab = {X.shape[1]}")
v = TfidfVectorizer().fit(docs)
print("既定の語彙:", v.get_feature_names_out().tolist())
scikit-learn 1.6.1 での出力は次のとおりです。
既定のTfidfVectorizer: cos = 0.000 vocab = 4
char_wb 2-3gram: cos = 0.623 vocab = 36
既定の語彙: ['やり方', 'キーワード', 'キーワードクラスタリング', 'クラスタリング']
語彙を見れば原因は明白です。「キーワードクラスタリング」と「キーワード」「クラスタリング」が、別々の独立した特徴量として登録されています。検索する人が空白を入れたかどうかだけで、同じ意図のクエリが共通語彙をひとつも持たなくなります。共通語彙がなければ内積は0、つまりコサイン類似度も0です。日本語のキーワードは空白の有無が揺れるため、この罠はほぼ確実に踏みます。
文字N-gramと形態素解析、どちらを選ぶかの分岐点
analyzer="char_wb" は文字単位のN-gramを特徴量にするため、token_pattern による語の切り出しを行いません。上の実測では2〜3文字のN-gramで類似度が0.623まで回復しました。辞書もインストールも不要で、キーワードのような短い文字列にはこれで十分です。
ただし char_wb は空白を境界として各語をパディングする仕様のため、空白をまたぐN-gramは生成されません。「キーワード クラスタリング」を3文字のN-gramにすると、char_wb では「ド ク」のような空白をまたぐ特徴量が0件になるのに対し、analyzer="char" では生成されます。ただし空白を一切含まないわけではなく、語の端をパディングした「 キー」「ング 」のような特徴量は作られます。語順や複合語のつながり方まで見たい場合は char を選んでください。
形態素解析器(MeCab、Sudachi、Janome など)を導入する価値があるのは、表記ゆれの正規化まで必要な場合に限られます。「サーチコンソール」と「Search Console」のように文字種ごと違う表記を同一視したいなら、文字N-gramでは届かず、辞書による正規化が要ります。送り仮名や空白の揺れを吸収したいだけなら、辞書を持ち込むより文字N-gramのほうが安く済みます。
埋め込みモデルへ切り替える判断基準
文字N-gramは字面の重なりしか見ません。「被リンク」と「バックリンク」のように、文字がまったく重ならない同義語は別クラスタに落ちます。この種の同義語が多いなら、文埋め込みモデルへの切り替えを検討してください。日本語を含む多言語モデルでは intfloat/multilingual-e5-large(1,024次元)などがローカルで動き、API経由なら OpenAI の text-embedding-3-small(既定1,536次元、dimensions で縮約可)が選択肢になります。
切り替えの判断は目視で足ります。文字N-gramで作ったクラスタを一覧し、字面は違うが同義のペアが別クラスタに割れている件数を数えてください。本記事では全キーワードの1割を超えたら切り替えを目安にしています。それ未満なら、埋め込みの費用と実行時間を払う理由はありません。
KMeansとHDBSCANの選び分けと、クラスタ数を決める基準
ベクトルが用意できたら、束ね方を選びます。判断の軸は精度ではなく、全キーワードを必ずどこかに割り当てたいか、それとも属さないものを外れ値として切り離したいかです。クラスタ数の決め方には定番の指標がありますが、そのまま使うと判断を誤ります。
KMeansを選ぶ条件と、n_initの既定値
キーワードを重複なく漏れなく分割したい場合、KMeans が第一候補です。クラスタ数を先に決める必要がある代わりに、数万件規模でも実用的な速度で収束します。scikit-learn 1.6.1 では n_init の既定値が "auto" になっており、初期化戦略に応じて試行回数が自動で決まります。n_init=10 を明示していた古いコードをそのまま持ち込むと、意図せず既定と異なる挙動になるため、書き換えの際は確認してください。
シルエット係数を単独指標にできない理由と、乱数シードによる振れ幅
クラスタ数の決定には、シルエット係数が最大になるkを採る方法が定番です。ただしキーワードの件数が少ないうちは、この方法が判断材料になりません。次のコードで実際に測ります。入力は12件のキーワードで、乱数シードを固定しています。
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
kws = ["キーワード クラスタリング", "キーワードクラスタリング やり方", "キーワードクラスタリング ツール",
"サーチコンソール api python", "search console api 使い方", "search console api 認証",
"クラスタリング 手法 比較", "クラスタリング 手法 選び方", "kmeans クラスタ数 決め方",
"seo 内部リンク 最適化", "内部リンク 貼り方", "トピッククラスター 設計"]
X = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 3)).fit_transform(kws)
for k in range(2, 7):
km = KMeans(n_clusters=k, random_state=0).fit(X)
print(f"k={k} sil={silhouette_score(X, km.labels_):.3f} wcss={km.inertia_:.3f}")
scikit-learn 1.6.1 での出力は次のとおりです。
| クラスタ数 k | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|
| シルエット係数 | 0.114 | 0.177 | 0.178 | 0.188 | 0.208 |
| クラスタ内二乗和 | 7.917 | 6.265 | 5.156 | 4.040 | 2.902 |
この条件では係数がkとともに増え続け、最大値を採ると「1キーワード1クラスタ」へ向かいます。ただしこれを一般法則として受け取らないでください。random_state だけを変えて同じ入力を測り直すと、結果は次のように動きます。
| random_state | k=2 | k=3 | k=4 | k=5 | k=6 |
|---|---|---|---|---|---|
| 0 | 0.114 | 0.177 | 0.178 | 0.188 | 0.208 |
| 1 | 0.108 | 0.103 | 0.204 | 0.190 | 0.218 |
| 7 | 0.114 | 0.177 | 0.204 | 0.208 | 0.205 |
| 42 | 0.106 | 0.088 | 0.179 | 0.188 | 0.218 |
| 2026 | 0.063 | 0.117 | 0.204 | 0.209 | 0.218 |
k=3の係数はシードによって0.088から0.177まで動き、kに対して増え続けたのは5シード中2つだけでした。原因は n_init="auto" の挙動にあります。既定の init="k-means++" と組み合わせた場合、初期化は1回しか行われません。データ件数が少なければ、その1回の初期値がそのまま結果を左右します。加えてn=12でk=6を採ると1クラスタあたり2点となり、係数の解釈に耐えません。
したがって係数は単独の判断材料にせず、1クラスタあたりのキーワード数から先にkの範囲を絞ってください。本記事では、1本の記事で受け切れる分量として1クラスタ5〜20件を目安に置いています。この範囲に収まるkの候補をいくつか出し、その中でシードを変えても安定して係数が高いものを選ぶ手順が現実的です。
HDBSCANでノイズを切り離す条件
KMeans は全キーワードをいずれかのクラスタに強制的に割り当てます。どこにも属さない孤立したクエリも例外ではありません。結果として、少数の外れ値が特定クラスタの重心を引っ張ります。sklearn.cluster.HDBSCAN はクラスタ数の事前指定が不要で、外れ値に -1 のラベルを付けて分離します。前掲と同じ12件のキーワードに適用すると、4つのクラスタと2件のノイズに分かれました。疎行列はそのまま渡せるので、追加の変換は要りません。
from sklearn.cluster import HDBSCAN
# X は前掲の char_wb ベクトル(疎行列のまま渡せる)
print(HDBSCAN(min_cluster_size=2).fit_predict(X).tolist())
# -> [2, 2, 2, 0, 0, 0, 3, 3, -1, 1, 1, -1]
指名検索や誤入力に由来するクエリが混ざる実データでは、この分離が効きます。一方、すべてのキーワードを必ずどこかの記事に割り当てたい場合は、ノイズ扱いされた分を手作業で拾う手間が増えるため、KMeans のほうが運用は楽です。クラスタの可視化まで進めるなら、次元削減の実装はt-SNEによる3D可視化とSEOデータのPython実装で扱っています。
クラスタを記事単位の施策に落とす手順
クラスタができた時点では、まだキーワードの集合が並んでいるだけです。ここから記事に対応づけるには、クラスタに名前を付け、既存URLと突き合わせ、リライトか新規かを決める3工程を通します。順位を持つURLを動かす判断が含まれるため、この工程は自動化せず人が確認してください。
代表クエリに重心最近傍を使わない理由と、impressions基準での選び方
クラスタに名前を付ける際、重心に最も近いキーワードを機械的に選ぶ方法がありますが、これは推奨しません。重心付近には短く一般的な語が集まりやすく、記事のタイトルとして具体性を欠くためです。
使うのはSearch Consoleが返す2つの指標だけで足ります。クラスタ内で impressions が最大のクエリを代表に採り、あわせて position が11〜30の範囲にあるクエリを併記します。平均掲載順位は数値が大きいほど下位を意味し、2ページ目前後のこの帯は、評価の土台がありながら流入に届いていない層です。代表クエリでその記事の主題を決め、併記したクエリで加筆すべき論点を決める、という読み方になります。
既存URLとの突き合わせによるカニバリの検出
データ取得時に page 次元を一緒に取っておいた理由がここです。1つのクラスタに対して表示されているURLが複数ある場合、同じ意図を複数の記事で受けている状態、つまりカニバリゼーションです。クラスタ単位でURLを集計し、表示回数の1位と2位が拮抗しているクラスタを抽出してください。
import pandas as pd
from sklearn.cluster import KMeans
# rows: 前掲のAPI取得結果(keys = [query, page])
df = pd.DataFrame([
{"query": r["keys"][0], "page": r["keys"][1],
"impressions": r["impressions"], "position": r["position"]}
for r in rows
])
# X は前節のベクトル化結果。k はクラスタサイズから決めた値
labels = KMeans(n_clusters=k, random_state=0).fit_predict(X)
df["cluster"] = labels
df = df[df["cluster"] >= 0] # HDBSCAN 併用時はノイズ(-1)をここで落とす
agg = (df.groupby(["cluster", "page"], as_index=False)["impressions"].sum()
.sort_values(["cluster", "impressions"], ascending=[True, False]))
top2 = agg.groupby("cluster").head(2)
share = top2.groupby("cluster")["impressions"].apply(
lambda s: s.iloc[1] / s.iloc[0] if len(s) > 1 else 0.0
)
print(share[share > 0.5]) # 本記事の目安: 2位が1位の5割超なら要確認
ここで2位が1位の5割を超えるクラスタは、統合か役割分担かの判断が必要です。この5割という水準は本記事が運用上の目安として置いたもので、公式の基準ではありません。片方を残してもう片方から内部リンクを送る形にするか、担当するクエリの範囲を明示的に分けるかを決めます。統合を選ぶなら、残す側が両方のクエリを本文で受け切れるかを先に確かめてください。評価が集まる仕組みはリンクジュースの仕組みとSEO効果で整理しています。
リライトと新規作成の判断基準
クラスタごとに、既存URLの平均掲載順位で扱いを分けます。11位から30位に既存URLがあるクラスタは、そのURLのリライトが最短です。既に評価の土台があるため、クラスタ内で拾えていないクエリを本文に補うだけで動きます。既存URLがまったく表示されていないクラスタだけを新規作成に回してください。表示回数が大きいという理由で新規記事を立てると、既にあるURLと自分で競合します。
複数のクラスタが1つの上位概念でまとまる場合は、記事を並べるのではなくハブとなる1本を立てて配下から内部リンクを集める構成が有効です。この設計はピラーページとトピッククラスター戦略で扱っています。クラスタで拾ったクエリを本文のどこに配置するかは、コンテンツSEOのキーワード最適化と共起語分析が実務的です。
キーワードクラスタリングを実装すべきでない条件
この手法が向かない状況を明示します。まず、対象キーワードが少ないサイトでは実装しないでください。本記事では200件を目安に置いています。担当者が一覧を上から下まで読み通せる規模なら、スプレッドシートで並べ替えて目で分類したほうが速く、しかも意図のズレに気づける分だけ精度が高くなります。クラスタリングが人力を上回るのは、全体を一望できなくなってからです。
次に、Search Console の表示回数が全体として少ないサイトも対象外です。前述の欠損は、母数が小さいほど致命的になります。残ったデータが実際の検索需要を代表しないためで、この場合はキーワードツールの検索ボリュームを入力に使うべきです。Search Console のデータをクラスタリングする前提が成り立ちません。
最後に、クラスタリングの結果をそのまま記事構成にする運用も避けてください。機械が束ねるのは文字列や埋め込みベクトルの近さであり、検索する人の目的ではありません。「〇〇 とは」と「〇〇 料金」は字面が近くても、前者は理解、後者は比較検討であり、同じ記事で満たせません。クラスタは候補の提示として扱い、意図が混ざっていないかは検索結果を見て人が確認する工程を必ず挟みます。
よくある質問
クラスタリングという言葉は、SEOでは何を指しますか?
一般には、正解ラベルを与えずにデータを類似性だけでグループへ分ける手法の総称で、教師なし学習に分類されます。SEOの文脈では対象が検索クエリに限定され、意味の近いクエリを束ねて1グループを1記事に対応づける作業を指します。手法そのものの体系や、階層型と非階層型の違いについては樹形図で階層的クラスタリングを読み解く解説記事を参照してください。
クラスタリングの手法はどれを選べばよいですか?
判断は一点だけです。分類できないキーワードが残ることを許容できるなら HDBSCAN、全件をどこかに割り当てたいなら KMeans を選んでください。前者はクラスタ数の指定が不要な代わりに拾い切れなかった分の手作業が発生し、後者は逆に、孤立したクエリまで無理に押し込みます。判断の根拠となる両者の挙動は本文で詳しく扱っています。
サーチコンソールのAPIで何行まで取得できますか?
1回のリクエストで25,000行です。Search Analytics API の rowLimit は有効範囲が1〜25,000、既定値は1,000で、指定を省くと1,000行で切れます。これを超える分は startRow を25,000ずつ進めて取得し、返却行数が rowLimit 未満になった時点が終端です。取得件数の上限そのものはドキュメントに記載がないため、実データで打ち切り位置を確認してください。
クラスタごとの表示回数を合計するとサイト全体の数値と合わないのはなぜですか?
匿名化クエリが原因です。Googleはプライバシー保護のため一部のクエリを匿名化し、公式ドキュメントによればそれらは長さ0の文字列として報告されます。表示回数やクリック数は持つ一方でクエリ文字列を持たないため、クエリ単位で足し上げた合計はサイト全体の数値に届きません。加えて、実行回数が非常に少ないクエリはそもそも記録されず、内部的な制限により保存される行も上位のものに限られます。いずれも仕様であり、設定の誤りではありません。クラスタ間の相対比較には使えますが、全体に対する比率としては扱わないでください。
データの可視化まで行う必要はありますか?
クラスタ数の妥当性を人が判断する工程では有効です。シルエット係数だけではクラスタが意味的に分かれているか判断できないため、2次元または3次元に落として重なり具合を見る手順が補助になります。ただし可視化は次元削減を伴い、削減後の距離は元の距離と一致しません。クラスタの分割そのものは削減前の空間で行い、可視化は確認用と割り切ってください。削減後の空間で散布図を描く手順はt-SNEを使った3次元可視化の実装解説にまとめています。