AIツールの請求CSVから未使用の有料席をPythonで洗い出す
2026年7月、Oktaの企業向けAI指数では「単一のAI提供者だけを使う会社」が前月から1.2ポイント減ったと出ています。現場の感覚でも、ChatGPTだけだったチームにClaude、Gemini、Notion AIが重なっていく。月額が数千円でも、席数が増えると経費の見え方が急に悪くなります。
僕は予算会議でAIの利用率を聞く前に、有料席の最終利用日を出す方が先だと思っています。解約候補を決めるためではありません。契約の持ち主、使っている部署、代替手段を確認するための、短い一覧を作るためです。
ここでは請求CSVと利用ログCSVを突き合わせ、45日以上動いていない有料席を出すPythonを載せます。外部APIは使いません。管理画面から出したCSVを、そのまま手元で確認する用途です。
先に結論:席数ではなく「席 × 最終利用日」で見る
AIツールの棚卸しでありがちな失敗は、請求書の席数だけを集計して終えることです。たとえば営業部に10席ある、と分かっても、その10席を誰がいつ使ったかまでは分かりません。更新月が近づいてから聞くと、使っている人も契約担当も記憶が曖昧です。
必要なのは次の二つだけです。
| CSV | 必須の列 | 役割 |
|---|---|---|
seats.csv |
email,tool,plan,monthly_yen |
請求対象の席を決める |
activity.csv |
email,tool,last_used_at |
最後に使った日を持つ |
同じ人が複数ツールを使うので、メールアドレス単独では結びません。email と tool の組で突き合わせます。ここをメールだけにすると、Geminiは使っているからNotion AIも使っている、と誤判定します。地味ですが、ここが一番効きます。
動く最小コード
下のコードはPython 3.12で実行し、後ろの出力になることを確認しました。記事内でサンプルCSVも文字列にしているため、まずはそのまま貼り付けて動かせます。本番では SEATS_CSV と ACTIVITY_CSV の中身を、CSVファイルから読んだ文字列に置き換えてください。
import csv
from datetime import date, datetime, timedelta
from io import StringIO
SEATS_CSV = """email,tool,plan,monthly_yen
sales@example.com,ChatGPT,Team,3000
sales@example.com,Notion AI,Plus,1500
planner@example.com,Gemini,Advanced,2900
designer@example.com,Claude,Pro,3000
planner@example.com,Cursor,Pro,3000
"""
ACTIVITY_CSV = """email,tool,last_used_at
sales@example.com,ChatGPT,2026-07-21T09:12:00
sales@example.com,Notion AI,2026-05-31T18:40:00
designer@example.com,Claude,2026-06-07T10:05:00
planner@example.com,Cursor,2026-06-08T09:30:00
planner@example.com,Cursor,2026-07-20T11:10:00
"""
TODAY = date(2026, 7, 23) # 実運用では date.today() にする
INACTIVE_DAYS = 45
def read_csv(text, required_columns):
rows = list(csv.DictReader(StringIO(text)))
if not rows or not set(required_columns).issubset(rows[0]):
raise ValueError(f"CSVに必要な列がありません: {required_columns}")
return rows
seats = read_csv(SEATS_CSV, {"email", "tool", "plan", "monthly_yen"})
activities = read_csv(ACTIVITY_CSV, {"email", "tool", "last_used_at"})
# 同じ席のログが複数あれば、いちばん新しい日時だけ残す
last_used = {}
for row in activities:
key = (row["email"].strip().lower(), row["tool"].strip())
used_at = datetime.fromisoformat(row["last_used_at"]).date()
last_used[key] = max(last_used.get(key, used_at), used_at)
cutoff = TODAY - timedelta(days=INACTIVE_DAYS)
candidates = []
for seat in seats:
key = (seat["email"].strip().lower(), seat["tool"].strip())
used_at = last_used.get(key)
# 利用記録なし、または45日以上前なら「確認候補」にする
if used_at is None or used_at <= cutoff:
candidates.append((seat, used_at))
total_yen = sum(int(seat["monthly_yen"]) for seat, _ in candidates)
print(f"判定基準: {cutoff.isoformat()} 以前を確認候補")
print(f"確認候補({len(candidates)}席 / 月額合計 {total_yen:,}円)")
for seat, used_at in candidates:
last = used_at.isoformat() if used_at else "利用記録なし"
print(
f"{seat['email']}\t{seat['tool']} {seat['plan']}\t"
f"最終利用: {last}\t{int(seat['monthly_yen']):,}円"
)
実行結果です。7月23日を基準にすると、6月8日以前の利用は確認候補になります。Cursorは6月8日と7月20日のログがありますが、後者を採用しています。
判定基準: 2026-06-08 以前を確認候補
確認候補(3席 / 月額合計 7,400円)
sales@example.com Notion AI Plus 最終利用: 2026-05-31 1,500円
planner@example.com Gemini Advanced 最終利用: 利用記録なし 2,900円
designer@example.com Claude Pro 最終利用: 2026-06-07 3,000円
「最新の1件」を取らないと候補が増える
利用ログCSVは、ログインのたびに1行増える形式が多いです。ここで最初に見つかった日付だけを使うと、Cursorのように6月8日と7月20日の両方にログがある席まで止まって見えます。コードでは max() で最新日だけを残しています。
この処理は小さく見えて、月次運用では差が出ます。古いログで誤って声をかけると、「毎日使っています」と返され、棚卸しそのものが信用されなくなるからです。候補の数を多く出すより、確認してほしい席を少なく正確に出す方が、担当者は動いてくれます。
今回の例では月額7,400円、年額に直すと88,800円です。ただ、金額が小さいからと後回しにすると、同じ席が翌月も残ります。金額の大小より、理由が空欄の席を残さないことを目的にしています。
対象の決め方がぶれなければ、確認依頼も毎月同じ型で回せます。
ツール名の表記ゆれにも注意が必要です。請求CSVが Google Gemini、利用ログが Gemini なら、このままでは別の製品として扱われます。まず10行ほど目で見て、製品名がそろっているか確認してください。そろわない製品だけは、CSVを直すか、対応表を一枚用意します。最初から複雑な名寄せを作る必要はありません。
45日を「停止」ではなく「確認」にする理由
利用ログがないことには、少なくとも三つ理由があります。本人が使っていない。SSO経由のログがCSVに含まれていない。あるいは、共有アカウントや別メールアドレスで使っている。この三つ目が意外に多いです。
昨日も経理の請求CSVを眺めていて、製品名の表記が「Notion」と「Notion AI」で分かれているのに気づきました。請求の切り口と利用ログの切り口は、案外そろいません。だから、このスクリプトの結果をそのまま停止リストにしない方が安全です。
候補が出たら、担当者には次の三点だけ聞きます。
- この席を直近45日で使ったか
- 別のメールアドレスや共有席で使っていないか
- 次の更新日まで残す理由があるか
回答を seats.csv に owner と review_note の列で残せば、翌月は確認がもっと速くなります。最初から完璧な資産台帳を作ろうとすると止まるので、まずは候補の3席を説明できる状態にします。
CSVを実データに替えるときの注意
日付の形式は揃えてください。このコードは 2026-07-21T09:12:00 のようなISO 8601形式を想定しています。管理画面が 2026/7/21 9:12 で書き出す場合は、datetime.fromisoformat() の前で形式を直す必要があります。
もう一つは、無料席を混ぜないことです。monthly_yen が0円の行を集計に入れると、確認件数だけが増えて担当者の手間が膨らみます。有料席だけを請求CSVから出せるなら、その方が話が早いです。
利用ログを取得できない製品もあります。その場合は「利用記録なし」と出るので、未使用と断定せず、契約担当が利用部門に確認します。判定を荒くしても、意思決定は人が持つ。この分け方なら、運用を始めやすいです。
で、現場でどう使うか
AIツールが増えるほど、契約の棚卸しは年1回では遅れます。Oktaの調査でも、企業が複数のAI提供者を併用する流れが強まっています。Okta Enterprise AI Index の数字は、大きな会社だけの話ではありません。
月初に請求CSVを落とし、このコードで確認候補だけを出す。席を止める判断より先に、使っている理由を回収する。これで、増えたツールを「なんとなく継続」にしにくくなります。これからは、導入の速さよりも、席ごとに説明できる状態の方が業務では効いてきます。