0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

生成AIの効果測定を利用回数から外すPython集計

0
Posted at

生成AIの効果測定を利用回数から外すPython集計

生成AIの利用回数が増えると、導入が進んだように見えます。でも営業や企画の現場で知りたいのは、開いた回数ではありません。仕事が終わるまでの時間が減ったか、手戻りを増やしていないかです。

7月28日には、Googleの利用データを扱った分析として「多くの仕事はAIで自動化されていない」という記事も出ていました。Ars Technicaの記事を読んで、利用率だけを月報に置く危うさをあらためて感じました。チャットの回数が増えても、成果が増えたとは限りません。

利用回数は、社内で使える人が増えたかを見る補助線にはなります。ただ、提案文を何度も作り直したり、出力を確認する時間が増えたりしても回数は伸びます。回数が高いほど順調、と読むと、現場で起きている遠回りを拾えません。

そこで僕は、作業ログを1タスク1行で残し、完了件数・実作業時間・手戻り率を同じ表で見るようにしています。下のスクリプトは、そのCSVをチーム別に集計するものです。Python標準ライブラリだけで動きます。

作業記録CSVを検査し、完了済みだけをチーム別に集計して次の改善工程を決める流れ

先に決めるCSVの形

列は次の8つです。baseline_minutes には、AIを使わなかった頃の同種業務で実際にかかった標準時間を入れます。見積もり時間ではなく、過去ログから決めるのがポイントです。

date,team,task_id,ai_used,baseline_minutes,actual_minutes,rework,status
2026-07-28,営業,S-101,yes,45,31,no,done
2026-07-28,営業,S-102,no,30,28,no,done
2026-07-28,営業,S-103,yes,60,42,yes,done
2026-07-28,企画,P-201,yes,90,65,no,done
2026-07-28,企画,P-202,no,50,55,no,done
2026-07-28,企画,P-203,yes,40,0,no,in_progress

statusdone 以外の行は集計しません。途中の仕事まで「AIで完了した件数」に入れると、導入直後ほど数字がよく見えてしまうからです。task_id の重複も止めます。同じ案件を二重に数えるミスは、表計算では意外と見つけにくいです。なお、AI利用の有無で難易度が違う案件を単純比較するための表ではありません。まずはチーム内の変化を追います。

Pythonでチーム別に出す

ai_effect_report.py として保存し、CSVと同じフォルダで実行します。

import csv
import sys
from collections import defaultdict

REQUIRED = {
    "date", "team", "task_id", "ai_used", "baseline_minutes",
    "actual_minutes", "rework", "status",
}
STATUSES = {"done", "in_progress", "blocked"}

def as_int(value, field, row_number):
    try:
        number = int(value)
    except ValueError as exc:
        raise ValueError(f"{row_number}行目の{field}は整数にしてください") from exc
    if number < 0:
        raise ValueError(f"{row_number}行目の{field}は0以上にしてください")
    return number

def as_flag(value, field, row_number):
    if value not in {"yes", "no"}:
        raise ValueError(f"{row_number}行目の{field}はyesかnoにしてください")
    return value == "yes"

def percent(numerator, denominator):
    return "-" if denominator == 0 else f"{numerator / denominator * 100:.1f}%"

def main(path):
    with open(path, newline="", encoding="utf-8-sig") as source:
        reader = csv.DictReader(source)
        if reader.fieldnames is None or not REQUIRED <= set(reader.fieldnames):
            missing = ", ".join(sorted(REQUIRED - set(reader.fieldnames or [])))
            raise ValueError(f"CSVの列が足りません: {missing}")

        seen_task_ids = set()
        teams = defaultdict(lambda: {
            "done": 0, "ai": 0, "baseline": 0, "actual": 0, "rework": 0,
        })
        for row_number, row in enumerate(reader, start=2):
            task_id = row["task_id"].strip()
            if not task_id:
                raise ValueError(f"{row_number}行目のtask_idが空です")
            if task_id in seen_task_ids:
                raise ValueError(f"task_idが重複しています: {task_id}")
            seen_task_ids.add(task_id)

            ai_used = as_flag(row["ai_used"], "ai_used", row_number)
            rework = as_flag(row["rework"], "rework", row_number)
            baseline = as_int(row["baseline_minutes"], "baseline_minutes", row_number)
            actual = as_int(row["actual_minutes"], "actual_minutes", row_number)
            status = row["status"]
            if status not in STATUSES:
                raise ValueError(f"{row_number}行目のstatusが不正です: {status}")

            if status != "done":
                continue
            team = row["team"].strip() or "未分類"
            report = teams[team]
            report["done"] += 1
            report["ai"] += ai_used
            report["baseline"] += baseline
            report["actual"] += actual
            report["rework"] += rework

    print("team,done_tasks,ai_tasks,ai_use_rate,baseline_minutes,actual_minutes,time_delta_minutes,rework_rate")
    for team, report in sorted(teams.items()):
        print(
            f"{team},{report['done']},{report['ai']},{percent(report['ai'], report['done'])},"
            f"{report['baseline']},{report['actual']},{report['actual'] - report['baseline']},"
            f"{percent(report['rework'], report['done'])}"
        )

if __name__ == "__main__":
    if len(sys.argv) != 2:
        raise SystemExit("使い方: python ai_effect_report.py work_log.csv")
    try:
        main(sys.argv[1])
    except (OSError, ValueError) as exc:
        raise SystemExit(f"エラー: {exc}")

実行はこれだけです。

python ai_effect_report.py work_log.csv

上の6行を使った出力はこうなります。進行中の P-203 が入っていないことも確認できます。

team,done_tasks,ai_tasks,ai_use_rate,baseline_minutes,actual_minutes,time_delta_minutes,rework_rate
企画,2,1,50.0%,140,120,-20,0.0%
営業,3,2,66.7%,135,101,-34,33.3%

time_delta_minutes は実作業時間から標準時間を引いた値です。負の値なら、そのチームでは時間を短くできています。ただし営業は34分短縮でも、手戻り率が33.3%です。この数字だけで「うまくいった」とは言えません。むしろ、下書き作成では効いているがレビュー前の確認が足りない、と次に見る場所が決まります。ここ地味に効きます。

今朝、手元のテスト用6行でこの出力を確認した時も、進行中の1行を除く処理と、重複した task_id で止まる処理を分けて見ました。集計ロジックは一度混ざった数字を後から直すより、入口で止める方がずっと楽です。

使う時に残したい前提

この集計は因果関係を証明するものではありません。難しい案件がたまたまAI利用側に偏れば、時間も手戻りも悪く出ます。案件の種類が違うチーム同士を比べる用途にも向きません。

最初の1か月は、同じチームの前月比を見るだけで十分です。baseline_minutes の決め方を途中で変えないこと、手戻りの定義をチームでそろえること。この2つを守ると、数字が会議用の飾りで終わりません。

で、現場でどう使うか

生成AIの導入効果は、利用回数のグラフだけでは判断できません。完了した仕事に限定して、時間差と手戻りを横に置くと、次に直すべき工程が見えてきます。

まずは今週の定例作業を5件だけ記録すれば足ります。数字が小さいうちは結論を急がず、同じ形式で続ける。AIを使うこと自体より、仕事が前に進んだかを見えるようにする方が、営業や企画には長く残ると思っています。

0
0
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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?