LoginSignup
15
2

More than 1 year has passed since last update.

WordCloud でコードレビューコメントの傾向を可視化して改善につなげる

Posted at

この記事は カオナビ Advent Calendar 2021 6日目です!

はじめに

アプリケーション開発において、コードレビューのプロセスを導入している開発組織がほとんどかと思いますが
組織が大きくなると複数人で分担してレビューすることになり、レビュワーによって観点が多少ブレてしまうことがあります。

また、ESLint 等の静的解析ツールだけでは担保できない観点については
ドキュメントなどで明文化することがレビュワー・レビューイ双方のコスト削減のために重要だと考えています。

今回はコードレビューのコメントの傾向を分析することで
現状はルール化できておらず、レビュー観点として明文化すべき事項がないかを探ってみたいと思います!

WordCloud とは

WordCloud は文章中の単語を出現頻度に比例した大きさで雲のように並べたものです。

こういうのです↓
sample.png

この例では「私」, 「事」, 「人」あたりが頻出する単語ということになります。

今回は Python の wordcloud ライブラリを使用して生成していきます。

WordCloud 生成手順

下記の手順で生成していきます。

  1. コードレビューコメントの収集
  2. コードレビューコメントを形態素解析
  3. WordCloud 生成

WordCloud に渡すテキストデータはスペース区切りである必要があります。
日本語を利用するために、形態素解析によってテキストを意味を持つ最小の単位に分割しスペース区切りの文字列へと変換します。

今回は形態素解析のライブラリとして Janome を使用します。

環境構築

マシン上に直接あれこれ入れたくないタイプなので、今回は Docker でさくっと構築していきます!
作成するファイルは同じディレクトリに配置してください。

まず Dockerfile は以下の通りです。

Dockerfile
FROM python:3

RUN apt update -y && apt install -y fonts-noto-cjk
RUN pip install wordcloud && pip install janome

fonts-noto-cjk は WordCloud 生成時のフォント指定に利用します。

Docker イメージのビルドとプログラムの実行を Makefile にまとめておきます。

Makefile
build:
    docker build . -t wc_sample
run:
    docker run --rm -it -v $(PWD):/usr/src/myapp -w /usr/src/myapp wc_sample python main.py

動作確認のために、main.py を以下の内容で作成します。

main.py
print('Hello WordCloud')

それでは動作確認をしてみます。

# イメージのビルド
$ make build

# main.py の実行
$ make run
Hello WordCloud

Hello WordCloud と出力されていれば問題ありません。

Janome で形態素解析してみる

Tokenizer

まずは適当な文章を Janome の Tokenizer を使って形態素解析してみましょう。

main.py
from janome.tokenizer import Tokenizer

text = 'この記事は カオナビ Advent Calendar 2021 6日目です!'

tokenizer = Tokenizer()

for token in tokenizer.tokenize(text):
    print(token)

make run します。

この  連体詞,*,*,*,*,*,この,コノ,コノ
記事  名詞,一般,*,*,*,*,記事,キジ,キジ
は 助詞,係助詞,*,*,*,*,は,ハ,ワ
    記号,空白,*,*,*,*, ,*,*
カオナビ    名詞,一般,*,*,*,*,カオナビ,*,*
    記号,空白,*,*,*,*, ,*,*
Advent  名詞,固有名詞,組織,*,*,*,Advent,*,*
    記号,空白,*,*,*,*, ,*,*
Calendar    名詞,固有名詞,組織,*,*,*,Calendar,*,*
    記号,空白,*,*,*,*, ,*,*
2021    名詞,数,*,*,*,*,2021,*,*
    記号,空白,*,*,*,*, ,*,*
6   名詞,数,*,*,*,*,6,*,*
日 名詞,接尾,助数詞,*,*,*,日,ニチ,ニチ
目 名詞,接尾,一般,*,*,*,目,メ,メ
です  助動詞,*,*,*,特殊・デス,基本形,です,デス,デス
! 記号,一般,*,*,*,*,!,!,!

このように単語を意味のある最小の単位で分割し、品詞と一緒に出力します。

Analyzer

正規化や特定の品詞のみを抽出したい場合など、形態素解析の前後で処理を行うには Analyzer を使用します。

先程と同じテキストに対し、今度は英単語は小文字に統一しつつ名詞のみを抽出してみます。

main.py
from janome.analyzer import Analyzer
from janome.tokenfilter import *

text = 'この記事は カオナビ Advent Calendar 2021 6日目です!'

token_filters = [POSKeepFilter(['名詞']), LowerCaseFilter()]

analyzer = Analyzer(token_filters=token_filters)

for token in analyzer.analyze(text):
    print(token)

make run します。

記事  名詞,一般,*,*,*,*,記事,キジ,キジ
カオナビ    名詞,一般,*,*,*,*,カオナビ,*,*
advent  名詞,固有名詞,組織,*,*,*,advent,*,*
calendar    名詞,固有名詞,組織,*,*,*,calendar,*,*
2021    名詞,数,*,*,*,*,2021,*,*
6   名詞,数,*,*,*,*,6,*,*
日 名詞,接尾,助数詞,*,*,*,日,ニチ,ニチ
目 名詞,接尾,一般,*,*,*,目,メ,メ

名詞のみ、かつ英単語が小文字になっているのがわかります。

他にも単語の出現回数をカウントする TokenCountFilter など、様々なフィルターが用意されています。

コードレビューコメントの取得

カオナビでは GitHub ではなく GitLab をホスティングして利用しています。
GitLab の GraphQL エンドポイントからマージリクエスト(以降 MR)とそのコメントの一覧を取得し、ファイルに書き出していきます。

取得の対象は「2021年にフロントエンドメンバーの作成した MR」とします。

今回はライブラリの紹介のみとし、実装したコードの詳細は割愛させていただきますmm

ライブラリ

ここまでの Python の流れを無視して使い慣れた TypeScript で実装します。

  • typescript: 人権
  • ts-node: TypeScript のコードをコンパイルなしで実行
  • graphql: GraphQL 実装
  • graphql-request: シンプル&軽量な GraphQL Client

Node.js でちょっとした GraphQL のリクエストを実行するときは ts-nodegraphql-request を利用するのが手っ取り早いように思います!

コメント取得の手順

コメント取得の流れとしては下記になります。

  1. GraphQL にて MR 一覧の取得
  2. MR のフィルター: フロントエンドメンバーの MR のみに
  3. コメントのフィルター: システムによるコメントと、MR 作成者本人のコメントを除去
  4. 前回までに取得されたコメント一覧にマージ
  5. 終了判定: 2020 年の MR を含む場合は終了し、含まない場合は 1番に戻る
  6. コメントを改行区切りで結合し、ファイルに出力

MR 作成者本人のコメントを除去するのは、レビュワーのコメントに対する返信やコードの補足コメントなどを除きたいからです。

取得したコメントをテキストファイルとして review_comment.txt に保存します。
実際の分析時はこのテキストファイルを読み込み、形態素解析していきます。

ざっくりこのような処理です。

const reviewComments = await fetchReviewComments();
const reviewCommentsText = reviewComments.join("\n");
fs.writeFileSync("review_comment.txt", reviewCommentsText);

GraphQL リクエストの実装例

graphql-request を使用した GitLab のエンドポイントへのリクエスト実装例です。

GitLab で GraphQL を利用する際はこの辺りのドキュメントを確認するか
GraphiQL でポチポチしてみるのがよいです。

import { gql, GraphQLClient } from "graphql-request";

const query = gql`
  query fetchMergeRequests($projectPath: ID!, $after: String) {
    project(fullPath: $projectPath) {
      name
      mergeRequests(state: all, after: $after) {
        pageInfo {
          endCursor
          hasNextPage
        }
        edges {
          cursor
          node {
            id
            createdAt
            author {
              name
            }
            notes {
              edges {
                node {
                  author {
                    name
                  }
                  body
                  createdAt
                  system
                }
              }
            }
          }
        }
      }
    }
  }
`;

const GRAPHQL_ENDPOINT = "https://gitlab.your-company.jp/api/graphql";

const graphQLClient = new GraphQLClient(GRAPHQL_ENDPOINT, {
  headers: {
    authorization: `Bearer ${process.env.GITLAB_ACCESS_TOKEN}`,
  },
});

const fetchMergeRequests = ({
  project,
  after,
}: {
  project: string;
  after?: string;
}) => {
  return graphQLClient.request<ResponseType>(query, {
    projectPath: project,
    after,
  });
};

パラメータの projectPath にはリポジトリ名を, after には前回リクエスト時の endCursor を渡します。
after を指定することで前回リクエストで取得されたデータの続きから取得することが可能です。

ResponseType はリクエストのレスポンスの型定義を渡します(ここでは割愛)。

WordCloud の生成

それでは取得したコードレビューのコメントから WordCloud を生成していきます。
最終的なコードは以下になります。

main.py
from wordcloud import WordCloud
from janome.analyzer import Analyzer
from janome.tokenfilter import *

f = open("review_comment.txt")
file_content = f.read()

token_filters = [POSKeepFilter(['名詞', '動詞', '形容詞', '副詞']),
                 LowerCaseFilter(),
                 ExtractAttributeFilter('surface') # 表層形を抽出
                ]

a = Analyzer(token_filters=token_filters)

# このコードには記載していないが、メンバーのユーザー名なども除外
stop_words = [
    'image', 'dev', 'imo', 'fyi', 'must',
    'lgtm', 'lgtmoon', 'https', 'gitalab', 'jp', 'cdn',
    'gitlab kaonavi', 'kaonavi',
    'の', 'それ', 'さそう', 'こと', 'よう', 'そう',
    'さ', 'ほう', 'ここ', 'おく', 'の', 'あと',
    'する', 'し', 'いい', '対応', '思い', 'こちら',
    'いる', 'なる', 'いる', 'ある', 'const', '場合', '方',
    'れ', 'よい', '感じ', 'ない', 'eye', 'eyes', 'い', 't', 'これ',
    'れる', 'ん', '思う', 'できる', 'てる', '気', 'わかる',
    'お願い', 'ちゃう', 'いただく', '返す', 'あげる',
    'ts', 'ため', 'せる', '使う', 'いただける', 'みたい', 'やる',
    'なっ', 'よ', 'でき', 'せ', 'とき', 'て', 'どう',
    'しれ', 'おい', 'なり', '時', 'こと', 'しまい', 'あり', 'しまう',
    'しまっ', 'ところ',
]

# 形態素解析 -> stop_words を除外
words = [token for token in a.analyze(file_content) if token not in stop_words]

# wordcloud が単語として識別できるようにスペースで区切る
text = ' '.join(words)

# 日本語のフォントのパスを指定
font_path = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'

# WordCloud 生成
wordcloud = WordCloud(background_color="white",
                      font_path=font_path,
                      width=1280,
                      height=720).generate(text)

# 画像として出力
wordcloud.to_file("./wordcloud.png")

make run を実行すると wordcloud.png が生成されます!

ポイントは制度をあげるために結果から不要な単語(ストップワード)を除外することです。

実際に出力してみて、不要そうな単語を stop_words に追加していったり
SlothLib などの日本語のストップワードのリストが用意されたライブラリを利用するのもよいです。

成果物

結果こちらの WordCloud が生成されました!

wordcloud.png

目立つ単語は下記でしょうか・・・

  • テスト
  • 必要
  • 指定
  • 確認
  • 不要
  • 定義
  • 表示

review_comment.txt 内でこれらの単語を検索してみると、もう少し具体的な傾向がわかります!

たとえば、「テスト」であれば

  • テストの命名
  • テストに利用するデータパターン
  • 新規実装に対するテスト有無

辺りについてがよくコメントされているようでした。

以前より認識はしていましたが、テストに関するルールをちゃんと明文化することで
コードレビューのコストが下げられそうだということが改めてわかりました。

おわりに

今回はコードレビューコメントに対して分析してみましたが、他にも色々なテキストに対して活用できそうです。

例えば、バグ報告のチケットや Slack のチャンネルの投稿を対象に解析してみると
ざっくりとした傾向は見えてくるかもしれません・・・!

テキストマイニングはなんとなく難しい前提知識が必要だと敬遠しがちなところがありますが
簡単な分析であれば公開されているライブラリでも気軽に試してみることができそうでした。

今回は映えを重視したところもあり結果を WorkCloud で表現しましたが
前述の Janome の TokenCountFilter による出現回数のリストを出してみるだけでもよいと思います。

分析結果を参考に、よりハッピーな開発環境を目指していきたいと思います💪

参考

15
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
2