はじめに
この記事は、LIFULL Advent Calendar 2025 16日目の記事です。
はじめまして、エンジニアの田中です。普段はLIFULL HOME'Sで中古住宅売買領域の開発を担当しています。
今回は、Pull Request(PR)の情報を用いてメンバーのレビュー活動を可視化するツールを作成しました。本記事では、その実装方法と実際に可視化した図を紹介します。
背景
このツールを作成した背景は主に2つあります。
1. 評価資料の準備
評価面談や目標面談の時期が近づくと、自分の貢献度を客観的に示せる指標があると便利だと感じることがあります。口頭で説明するよりも、具体的な数値や図があると客観的に貢献度を伝えられます。
2. レビュー依存度の可視化
この手法のもう1つの目的は、チーム内における特定メンバーへのレビュー依頼の偏りを可視化することです。実際にこれが実現すれば、レビューの偏在化に策を講じることができるのではないかと考えました。
完成イメージ
レビュー件数の推移
期間ごとの活動量を、数値とグラフで確認できます。
レビュー関係性マトリックス
誰が誰のPRをレビューしているかを一覧で表示し、特定メンバーへの依存度を視覚的に確認できます。
レビュー関係性ネットワーク図
D3.jsを使ってインタラクティブに操作が可能です。矢印の太さでレビュー頻度を表現し、依存関係を視覚的に把握できます。
技術的なポイント
GitHub API (GraphQL) の活用
GitHub CLIのgh api graphqlコマンドを使ってPR情報を取得します。
レビューしたPRの検索:
gh api graphql --paginate -f query="
query(\$endCursor: String) {
search(query: \"org:your-org is:pr merged:2025-11-01..2025-11-25 reviewed-by:username\", type: ISSUE, first: 100, after: \$endCursor) {
nodes {
... on PullRequest {
number
title
url
repository {
nameWithOwner
}
reviews(first: 100) {
nodes {
author {
login
}
state
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
"
作成したPRの検索:
gh api graphql --paginate -f query="
query(\$endCursor: String) {
search(query: \"org:your-org is:pr author:username merged:2025-04-01..2025-09-30\", type: ISSUE, first: 100, after: \$endCursor) {
nodes {
... on PullRequest {
number
reviews(first: 100) {
nodes {
author {
login
}
state
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
"
GraphQL APIのポイント:
-
search: GitHubの検索クエリを使用-
org:your-org: 組織を指定 -
is:pr: Pull Requestのみ -
merged:2025-11-01..2025-11-25: マージ済みPRを期間指定 -
reviewed-by:username: レビュアーを指定 -
author:username: PR作成者を指定
-
-
--paginate: 自動的にページネーションを処理 -
reviews: 各PRのレビュー情報を取得-
author.login: レビュアーのユーザー名 -
state: レビューの状態(APPROVED, CHANGES_REQUESTED等)
-
組織や期間といった指定は実行コマンドの引数として別途渡すことで、「特定リポジトリで、ここからここまでの期間のPR状況が見たい!」といったことも簡単に叶えられますね。
(例:./scripts/count_author_prs.sh 2025-10-01 2025-10-31)
レビュー関係性の取得
各メンバーのPRに対して、誰がAPPROVEDしたかを集計します。
# 特定ユーザーのPRとレビュー情報を取得
prs=$(gh api graphql --paginate -f query="
query(\$endCursor: String) {
search(query: \"org:your-org is:pr author:username merged:2025-04-01..2025-09-30\", type: ISSUE, first: 100, after: \$endCursor) {
nodes {
... on PullRequest {
number
reviews(first: 100) {
nodes {
author {
login
}
state
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
")
# 各レビュアーがAPPROVEDした件数をカウント
for reviewer in "${REVIEWERS[@]}"; do
count=$(echo "$prs" | jq -s "[.[] | .data.search.nodes[] | select(.reviews.nodes[] | select(.author.login == \"${reviewer}\" and .state == \"APPROVED\"))] | unique_by(.number) | length")
echo "${author} → ${reviewer}: ${count}件"
done
このようにして、PR作成者とレビュアーの関係性を収集します。
キャッシュ設計
レビュー関係性のキャッシュ:
{
"period": "2023-04-01..2025-11-27",
"matrix": {
"username1": {
"username2": 66,
"username3": 4,
"username8": 10
},
"username2": {
"username1": 12,
"username8": 36
}
}
}
期間ごとの件数をキャッシュすることで、2回目以降は高速に表示できます。
HTML生成の工夫
HTML/CSSまわりは 美的センスが壊滅的なので 生成AIが優秀なので、プロンプトを書いて作ってもらっています。プレースホルダー方式で、HTMLのテンプレートとデータを分離しました。
テンプレート:
const data = __DATA_PLACEHOLDER__;
生成スクリプト:
import json
data = json.loads(cache_data)
with open(html_file, 'r', encoding='utf-8') as f:
content = f.read()
# ensure_ascii=Falseで日本語をそのまま出力
content = content.replace('__DATA_PLACEHOLDER__', json.dumps(data, ensure_ascii=False))
with open(html_file, 'w', encoding='utf-8') as f:
f.write(content)
sedでの置換では特殊文字のエスケープが複雑になるため、Pythonを使用しています。ensure_ascii=Falseを指定することで、日本語がUnicodeエスケープされずにそのまま出力されます。
動的な閾値計算
全データからパーセンタイルで閾値を自動計算し、色分けに使用しています。
# 全レビュー件数を配列に格納
all_counts=()
for author in "${REVIEWERS[@]}"; do
for reviewer in "${REVIEWERS[@]}"; do
if [[ "$author" != "$reviewer" ]]; then
count=$(jq -r ".matrix.\"${author}\".\"${reviewer}\" // 0" "$cache_file")
if [[ "$count" != "0" ]]; then
all_counts+=($count)
fi
fi
done
done
# ソート
IFS=$'\n' sorted_counts=($(sort -rn <<<"${all_counts[*]}"))
unset IFS
total=${#sorted_counts[@]}
# パーセンタイルで閾値を決定
threshold_very_high=${sorted_counts[$((total * 5 / 100))]} # 上位5%
threshold_high=${sorted_counts[$((total * 15 / 100))]} # 上位15%
threshold_medium_high=${sorted_counts[$((total * 30 / 100))]} # 上位30%
threshold_medium=${sorted_counts[$((total * 50 / 100))]} # 上位50%
threshold_medium_low=${sorted_counts[$((total * 70 / 100))]} # 上位70%
echo "閾値: 超高頻度=${threshold_very_high}, 高頻度=${threshold_high}, 中高頻度=${threshold_medium_high}, 中頻度=${threshold_medium}, 中低頻度=${threshold_medium_low}"
固定値ではなくパーセンタイルを使うことで、データ量に応じて適切な可視化ができます。レビュー依存度の高い関係性が自動的に強調表示されます。
ネットワーク図の実装
D3.jsを使ってインタラクティブなネットワーク図を実装しています。
ノードとリンクの生成:
// ノードを作成(ユーザーごと)
const nodes = data.users.map(user => {
let totalReviews = 0;
data.users.forEach(author => {
if (data.matrix[author] && data.matrix[author][user]) {
totalReviews += data.matrix[author][user];
}
});
return { id: user, totalReviews: totalReviews };
});
// リンクを作成(レビュー関係ごと)
const links = [];
data.users.forEach(author => {
data.users.forEach(reviewer => {
if (author !== reviewer && data.matrix[author] && data.matrix[author][reviewer]) {
const count = data.matrix[author][reviewer];
if (count > 0) {
links.push({
source: author,
target: reviewer,
value: count
});
}
}
});
});
力学モデルの設定:
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links)
.id(d => d.id)
.distance(d => {
// 相互レビュー件数に応じて距離を調整
const reverseLink = links.find(l =>
l.source.id === d.target.id && l.target.id === d.source.id
);
const mutualCount = d.value + (reverseLink ? reverseLink.value : 0);
if (mutualCount >= 100) return 80;
if (mutualCount >= 50) return 120;
if (mutualCount >= 20) return 160;
return 200;
})
)
.force("charge", d3.forceManyBody().strength(-300))
.force("center", d3.forceCenter(width / 2, height / 2));
相互レビュー件数が多いほどノード間の距離を近くすることで、関係性の強さを視覚的に表現しています。
矢印の太さと色:
svg.append("g")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke", d => {
if (d.value >= threshold_very_high) return "#c62828";
if (d.value >= threshold_high) return "#e53935";
if (d.value >= threshold_medium_high) return "#ef5350";
if (d.value >= threshold_medium) return "#e57373";
if (d.value >= threshold_medium_low) return "#fff59d";
return "#ccc";
})
.attr("stroke-width", d => Math.sqrt(d.value) * 0.5);
レビュー件数の平方根に比例させることで、極端に太い矢印を避けつつ、差を視覚的に表現しています。
実際に使ってみて
- 期間ごとの具体的な数値を提示できる
- グラフで視覚的に貢献度を示せる
- レビュー関係の偏りを可視化できる
ネットワーク図などを見ることで、今まで感覚的に持っていた「このメンバーに矢印が集中している」「この2人は相互レビューが多い」といった傾向が、定量的かつ視覚的に分かります。
まとめ
評価資料の準備とチーム内レビューの偏在化改善を目的に、GitHub PRの分析ツールを作成しました。
シェルスクリプトとGitHub CLIを組み合わせることで、手軽にレビュー活動を可視化できます。キャッシュ機能やプレースホルダー方式、動的な閾値計算など、実用的な工夫も盛り込んでいます。
同じような課題を抱えている方の参考になれば幸いです。


