python-gitlabを使ってアクティビティMatrixを作ってみる

今後大規模開発があるとかないとかで、gitlabを使うことになっています。が、だれがどう開発に貢献しているんだ？を知りたいという疑い深い要求www に答えてみます。

gitlabのユーザID概要を見るとアクティビティがあり、コミット/まーじリクエストなどの実行数がヒートマップで表示されています。

これを個人単位ではなくてプロジェクト参加者全員のアクティビティを表現するようにヒートマップを作ってみます。

## 目標

こんな感じのヒートマップ生成を目指します。
お試しプロジェクトなので参加者は2名しかいませんwww

![ActivityMatrix.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213343/9fd21cb3-4ed0-aa71-6158-cc3d2e4d9419.png)

## 必要要素
- gitlabを使用するので「python-gitlab」を使用する縛りとします。
- ヒートマップ作成は seabornを使用します

## python-gitlabの使い方
あまり事例がなかったり公式APIを見てもちょっとわかりにくいので試行錯誤した結果から書いたコードを簡単に説明しながら進めます。

### gitlabインスタンス生成
インスタンスパラメータは定番どおり。

```gl = gitlab.Gitlab(URL, PRIVATE_TOKEN)```

### プロジェクト（群）Object取得
GitlabインスタンスからプロジェクトObjectを生成します。
```projects = gl.projects.list(all=True)```

プロジェクト全体の情報をかき集めるためこのproject群でループして必要な情報を取得していきます。

### Project内のコミット情報を取得します
プロジェクト：コミット = 1: n になります。コミット単位での主要情報とコミット内で複数のファイル反映がありますので両方から得られる情報をうまいところ組み立てていく必要があります。
```commits = project.commits.list()```

## コミット内のdiff情報をを取得
```python
    for _ in commits:
        # コミット単位に情報を得る
        commit = project.commits.get(_.attributes['id'])
        
        # コミット毎のdiff情報を取得する
        for __ in commit.diff():
            ......
```

### 変更の明細（追加、削除）情報が欲しい
コミット単位内でファイル毎のdiffを行明細で取得が可能ですがレポートには此処の明細ではなく変更量の情報を取得するようにします。明細は万一の際にトレースできるようにしておきます。
```_list_temp = __['diff'].split('\n')```

### 追加、削除行を判定する
- 先頭行 ＋：追加
- 先頭行 - ：削除
の情報を持っていますのでこれを判定基準にします。
これらを変更対象ファイル名をkeyにしたmapへ格納します。

```python
# 追加、削除をカウント（対象コードも溜めてみる）
for ___ in _list_temp:
    if ___.startswith('+'):
        _list_add.append(__)
    if ___.startswith('-'):
        _list_del.append(__)
    _map[__['new_path']] = [_list_add, _list_del]
```

## いろいろ拾うので除外する
更新していないものも拾ってしまうので# 追加、削除がないものは対象外とする

```if len(v[0]) != 0 or  len(v[1]) !=0:```

## Pandas DataFrameに格納する
チョット罠があります。
タイムゾーンがバラバラのデータはdatetime型変換が期待どおりになりません（仕様w）。
なのでいったんUTCに統一してdatetime型に変換してそのあとさらにAsia/Tokyoに変換することになります。
めんどい www

わざわざdatetime型にしたのはresampleを使用するためです。

```python
# DataFrame化
df = pd.DataFrame(_list)
df.columns = columns

# タイムゾーンがばらばらなので一旦UTCで統一する
# datetime64[ns, UTC]になっている。
# tz統一なく to_datetime()を行ってもdatetime64型にならない
df['commited_date'] = pd.to_datetime(df['commited_date'],utc=True)
df['commited_date'] = df['commited_date'].dt.tz_convert('Asia/Tokyo')
df.set_index('commited_date', inplace=True)
print(df.info())
df.head()
```

![DataFrame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213343/2ce4bc7d-5115-63f9-6398-bef943333ff8.png)

## DataFrameを加工します
- 日単位でコミット数をコミッター毎に集計
- 見栄えのため日付け表示を加工
- pivot_tableで時系列編集
- NaNを0で置き換え
- 直近30日だけ抽出

```python
# コミッター毎に日単位でコミット回数を集計
df_x1 = pd.DataFrame(df.groupby(['Commiter']).resample('D').count()['hexsha'])
df_x1.reset_index(inplace=True)
# df['commited_date']を yyyy-mm-ddに変換する
df_x1['commited_date'] = [ _[0] for _ in df_x1['commited_date'].astype(str).str.split(' ') ]
DAY = 30
df4 = pd.pivot_table(df_x1, 
                     'hexsha', 
                     columns='commited_date', 
                     index='Commiter').fillna(0).iloc[:, -DAY:]
```
![DataFrame2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213343/0e105fba-7a21-8a4f-6acc-f77db2dd5a3e.png)

## 一か月のコミット数推移をヒートマップで表現します
ここまでくればあとはseabornのヒートマップを使って表現します。

```python
# 描画用ライブラリ導入
import  matplotlib.pyplot as plt
import seaborn as sns

# バランスを取りながら描画エリアを定義
fig, axes = plt.subplots(1, 1, figsize=(DAY/2, len(df4)))

# seabornのヒートマップを生成
sns.heatmap(df4,              # 対象DataFrame
            ax=axes,          # 描画枠適用
            cmap="Greens",    # GitHubっぽいカラーで
            annot=True,       # 数字を入れる
            cbar=False,       # カラーバーは入れない
            square=True,      # 四角になるよう形状調整
            linewidths=1.0    # BOXの間に白線枠を入れる
           )

# title, X/Y軸 ラベル設定
## X軸、Y軸共にラベル設定しない
axes.set_title(f'Gitlab Commit Count: Last {DAY} Days') # タイトル
axes.set_xlabel("")                                     
axes.set_ylabel("")
```

## まとめ
ちょこちょと罠があったりますがこれで開発参加者の貢献度が表現できますね。

## 考えること
開発スタイルを考えると貢献度という点ではコミット数ではなくて、実態の指標はマージリクエストの予感はします。マージリクエストの状況はpython-gitlabでも同じように取得できます。データの取り出し方、構成はおおよそcommit情報とおなじwwwかなと。

```python
# DataFrameカラム定義
columns = ['created_at','target_project_id', 'project_name', 'id', 'iid', 'title', 'description', 'target_branch', 'source_branch','username','state','merged_at','merged_by','web_url']

# コミット情報格納リスト
_list = []

# インスタンス生成
api = gitlab.Gitlab(URL, PRIVATE_TOKEN)
    
# ProjectIDtoProjectName map生成
map_ProjectIDtoProjectName = mapFromProjectIDtoProjectName(api)

# projectsとのコンボらしい
projects = api.projects.list(all=True)

# マージリクエスト情報を取り出す
for project in projects:

    # プロジェクト毎のマージリクエストを取得
    mrs = project.mergerequests.list(order_by='updated_at')

    # 個々のマージリクエスト情報を取得する
    for mr in mrs:
        _list.append([mr.attributes['created_at'],
                      mr.attributes['target_project_id'],
                      __map[mr.attributes['target_project_id']],
                      mr.attributes['id'],
                      mr.attributes['iid'],
                      mr.attributes['title'],
                      mr.attributes['description'],
                      mr.attributes['target_branch'],
                      mr.attributes['source_branch'],
                      mr.attributes['author']['username'],
                      mr.attributes['state'],
                      mr.attributes['merged_at'],
                      mr.attributes['merged_by'],
                      mr.attributes['web_url'],   
             ])

# DataFrameに組込む
df = pd.DataFrame(_list)
df.fillna('-', inplace=True)
df.columns = columns
df

```

## DataFrameを生成するコードのまとめ
ちょっとだけヘルパー関数も書いているのでそれらを含めてまとめてコード掲載します。

```python
import gitlab
import pandas as pd
from datetime import datetime
from pprint import pprint

# ヘルパー関数 プロジェクトID→プロジェクト名のmap生成
def mapFromProjectIDtoProjectName(api):
    _map = {}
    projects = api.projects.list(all=True)
    for _ in projects:
        _map[_.attributes['id']] = _.attributes['name']
    return _map

# def
URL = 'http://xxx.xxx.xxx.xxx:xxxx'
PRIVATE_TOKEN = 'xxxxxxxxxxxxxxxxej_3X'

# DataFrameカラム定義
columns = ['commited_date','project_id', 'project_name', 'hexsha', 'Commiter', '追加行', '削除行', 'file_name', 'CommitMessage']

# インスタンス生成
try:
    gl = gitlab.Gitlab(URL, PRIVATE_TOKEN)
except Exception as e1:
    print(f'{e1}')
    
# ProjectIDtoProjectName map生成
map_ProjectIDtoProjectName = mapFromProjectIDtoProjectName(gl)


# コミット情報格納リスト
_list = []

# projectsとのコンボらしい
try:
    projects = gl.projects.list(all=True)
except Exception as e2:
    print(f'{e2}')
    
for project in projects:
    
    # プロジェクト単位にコミット毎の情報を取得する
    try:
        commits = project.commits.list()
    except Exception as e3:
        print(f'{e3}')
        
    for _ in commits:
        # コミット単位に情報を得る
        try:
            commit = project.commits.get(_.attributes['id'])
        except Exception as e4:
            print(f'{e4}')        
        
        # コミット毎のdiff情報を取得する
        for __ in commit.diff():

            # 選別処理を行う
            _map = {}
            _list_add = []
            _list_del = []

            # コード分離
            _list_temp = __['diff'].split('\n')

            # 追加、削除をカウント（対象コードも溜めてみる）
            for ___ in _list_temp:
                if ___.startswith('+'):
                    _list_add.append(__)
                if ___.startswith('-'):
                    _list_del.append(__)
                _map[__['new_path']] = [_list_add, _list_del]

            # 結果を蓄積
            for k, v in _map.items():
                
                # 追加、削除がないものは対象外とする
                if len(v[0]) != 0 or  len(v[1]) !=0:
                    # 蓄積
                    _list.append([_.attributes['committed_date'],
                                  _.attributes['project_id'],
                                  map_ProjectIDtoProjectName[_.attributes['project_id']],
                                  _.attributes['id'],
                                  _.attributes['committer_name'],
                                  len(v[0]),
                                  len(v[1]),
                                  k,                              
                                  _.attributes['title'],
                                 ])

# DataFrame化
df = pd.DataFrame(_list)
df.columns = columns

# タイムゾーンがばらばらなので一旦UTCで統一する
# datetime64[ns, UTC]になっている。
# tz統一なく to_datetime()を行ってもdatetime64型にならない
df['commited_date'] = pd.to_datetime(df['commited_date'],utc=True)
df['commited_date'] = df['commited_date'].dt.tz_convert('Asia/Tokyo')
df.set_index('commited_date', inplace=True)
print(df.info())

# 確認
df['Commiter'] = df['Commiter'].replace('すずきさとし','S.Suzuki')
df.head()


```