0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-06-29

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

集計例

image.png

やりたかったこと

目的

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

条件

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

実現方法

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

環境

  • Python 3.9.0
  • pandas 1.1.5
  • numpy 1.19.4

コード

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

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

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 を作る。

df = pd.DataFrame(issues)

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

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

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

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 にしている。
marginsTrue にすることで、 行・列の合計が計算されるようにしている。

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に変換したり、コピーしたりできる。

table

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

table / 8.0

関連

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

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'])
0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?