Backlog の時間を集計するスクリプト

nulab Backlog で 課題の予定時間を合計するためのスクリプトを作りました。 担当者別、ステータス別、カテゴリ別に集計します。 少しコードを変えれば、実績時間なども集計できます。参考になれば幸いです。

## 集計例

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25846/1eeb344e-ccb0-5d6e-8699-310e299932e7.png)


## やりたかったこと

### 目的

* Backlog のひとつのプロジェクト全体で、予定工数を人別に、ステータス別、カテゴリ別に集計したかった。

### 条件

* LLで素早く作りたい。 結構いろんな開発を同時進行で行なっているためIDEをもうひとつ立ち上げる気はなかった。

## 実現方法

* jupyter notebook を使う。（コードは jupyter notebook でなくても動くし、結果も取得可能）
* 課題のデータはまず全部取得して、その後の処理は Python の pandas を使う。 Pivot Table つくれるのがよい。

## 環境

* Python 3.9.0
* pandas 1.1.5
* numpy 1.19.4

## コード

改善点はいろいろあるけども、とりあえず動くものを作った。

まずはデータを取得する。

```python
import json
import pandas as pd
import urllib


class BacklogUrl:
    base = 'https://xxxxxx.backlog.xxx'
    key = '__API_KEY___'
    
    def __init__(self, path, param = {}):
        self.path = path
        self.param = param
    
    def to_str(self):
        text = BacklogUrl.base + self.path

        i = 0
        for k, v in self.param.items():
            if i == 0:
                text += '?'
            else:
                text += '&'
            text += f'{k}={v}'
            i += 1
        if len(self.param) == 0:
            text += '?'
        else:
            text += '&'
        text += f'apiKey={BacklogUrl.key}'
        i += 1
        
        return text
    
    def get(self):
        try:
            url = self.to_str()
            print(url)
            result = urllib.request.urlopen(url).read()
            print(result)
            return json.loads(result)
        except e:
            print(e.message)
            raise e

            
class Backlog:
    def get_project(key):
        return Project(BacklogUrl(f'/api/v2/projects/{key}').get()['id'])
        
        
class Project:
    def __init__(self, id):
        self.id = id
        
    def get_issues(self):
        count = self.get_issue_count()
        max_itr = (count + 99) // 100
        
        list = []
        for i in range(max_itr):
            list += BacklogUrl(
                f'/api/v2/issues',
                {
                    'projectId[]': self.id, 'sort': 'created', 'count': 100, 'offset': i * 100
                }
            ).get()
            
        return list
    
    def get_issue_count(self):
        return BacklogUrl(f'/api/v2/issues/count', {'projectId[]': self.id}).get()['count']
    
# プロジェクトのキーを引数にする
project = Backlog.get_project('ABCDE')
issues = project.get_issues()
```

Backlog サーバへはもうアクセスしない。
（今回はひとつのプロジェクトの中の課題を対象にしているが、プロジェクトを横断して集計することも可能。）

もしこのコードを試してみる場合は、お使いの Backlog の課題数に気をつけてください。
私の場合は幸いにしてまだ課題数が少なかったので全課題を取得しても問題ありませんでしたが、
あまりにも課題が多い場合はマシンの負荷で問題が出るかもしれません。

DataFrame を作る。 

```python
df = pd.DataFrame(issues)
```

DataFrame から集計対象データを抽出する。 子課題のみ対象とする。 （これは運用によっていろいろ条件があると思う。）

```python
childIssues = df[~df['parentIssueId'].isnull()]
```

カテゴリ名、担当者名、ステータス名が dict になっており 集計できないので、 集計のキーにするカラムを新しく作る。
名前は、 category1, assignee1, status1 とした。

```python
childIssues.loc[:, 'category1'] = childIssues.category.map(lambda x: x[0]['name'])
childIssues.loc[:, 'assignee1'] = childIssues.assignee.map(lambda x: x['name'] if x is not None else None)
childIssues.loc[:, 'status1'] = childIssues.status.map(lambda x: x['name'])
```

Pivot Table を作成する。
ここでは行の分類をカテゴリとし、列の分類を担当者とステータスにしている。 (行の分類についても複数指定可能。)

時間の合計値がほしいので、 `aggfunc` には `np.sum` を指定している。
テーブル全体を見ると、 合計値が null になるところがでてくるので `fill_value` を 0 にしている。
`margins` を `True` にすることで、 行・列の合計が計算されるようにしている。

```python
import numpy as np
table = pd.pivot_table(
    childIssues,
    index='category1', columns=['assignee1', 'status1'], values=['estimatedHours'],
    aggfunc=np.sum, margins=True, fill_value=0
)
```

jupyter notebook であれば、 `table` を実行すればテーブルが表示される。
jupyter notebook を使っていない場合は to_csv, to_clipboard などを使うとCSVに変換したり、コピーしたりできる。

```python
table
```

estimatedHours は時間単位なのでこれを日付単位にしたければ 8 で割って表示する。（1日8時間稼働できると仮定した場合。）

```python
table / 8.0
```

### 関連

次のようにすると、優先度ごとに、ひとごとに、終わった時間数、終わっていない時間数を表にできます。

```python
childIssues = df[~df['parentIssueId'].isnull()]
childIssues.loc[:, 'category1'] = childIssues.category.map(lambda x: x[0]['name'])
childIssues.loc[:, 'assignee1'] = childIssues.assignee.map(lambda x: x['name'] if x is not None else None)
childIssues.loc[:, 'status1'] = childIssues.status.map(lambda x: x['name'])
childIssues.loc[:, 'status1'] = childIssues.status1.map(lambda x: 'Completed' if x == 'Closed' else 'Not Completed')
childIssues.loc[:, 'priority1'] = childIssues.priority.map(lambda x: x['name'])
```