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

この記事は [カオナビ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/kaonavi) 6日目です！

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

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

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

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

こういうのです↓
![sample.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151404/59435c1e-f741-64a0-f4b6-de867997d9a2.png)

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

今回は Python の **[wordcloud](https://pypi.org/project/wordcloud/)** ライブラリを使用して生成していきます。

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

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

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

今回は形態素解析のライブラリとして **[Janome](https://pypi.org/project/Janome/)** を使用します。

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

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

```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: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` を以下の内容で作成します。

```py:main.py
print('Hello WordCloud')
```

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

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

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

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

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

```py: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** を使用します。

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

```py: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](https://about.gitlab.com/) をホスティングして利用しています。
GitLab の GraphQL エンドポイントからマージリクエスト（以降 MR）とそのコメントの一覧を取得し、ファイルに書き出していきます。

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

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

### ライブラリ
~~ここまでの Python の流れを無視して使い慣れた TypeScript で実装します。~~

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

Node.js でちょっとした GraphQL のリクエストを実行するときは [ts-node](https://github.com/TypeStrong/ts-node) と [graphql-request](https://github.com/prisma-labs/graphql-request) を利用するのが手っ取り早いように思います！

### コメント取得の手順

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

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

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

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

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

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

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

GitLab で GraphQL を利用する際は[この辺り](https://docs.gitlab.com/ee/api/graphql/)のドキュメントを確認するか
[GraphiQL](https://gitlab.com/-/graphql-explorer) でポチポチしてみるのがよいです。

```ts
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 を生成していきます。
最終的なコードは以下になります。

```py: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](http://svn.sourceforge.jp/svnroot/slothlib/CSharp/Version1/SlothLib/NLP/Filter/StopWord/word/Japanese.txt) などの日本語のストップワードのリストが用意されたライブラリを利用するのもよいです。

## 成果物
結果こちらの WordCloud が生成されました！

![wordcloud.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151404/077938aa-37fb-4a08-fe4b-7183aa6c4377.png)

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

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

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

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

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

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

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

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

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

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

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

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

## 参考
 - [Word Cloudでツイートを可視化してみた(python) - Qiita](https://qiita.com/turmericN/items/04cd0b40f91076f0ef42)
 - [便利すぎるjanomeのAnalyzerモジュール【自然言語処理の基礎】 | なんじゃもんじゃ](https://nanjamonja.net/archives/793)