Claude Media
Claude CodeでKAKEN APIの採択課題を集計する — 部局別・種目別の基礎表

Claude CodeでKAKEN APIの採択課題を集計する — 部局別・種目別の基礎表

KAKEN APIで機関の採択課題を取り、部局別・種目別の件数と配分額の表を作る手順です。部局名の表記ゆれと継続課題の二重計上の点検をClaude Codeに任せる設定とスクリプト付きです。

学内の科研費の採択状況を部局別・種目別に出す仕事は、毎年どこかの部署に回ってきます。KAKEN(科研費のデータベース)にはAPIがあるので、機関名と年度を指定すれば採択課題を機械的に集められます。

難しいのは取得ではなく、数え方です。継続課題を年度ごとに数えると二重になり、部局名は表記が揺れ、配分額は採択時の額です。Claude Codeには、集計スクリプトを書かせるだけでなく、この3点を点検させます。

この記事では、アプリケーションIDの用意から、取得・集計スクリプト、点検表の読み方、出典の書き方までを順に扱います。

集計の単位を先に決める

基礎表を作る前に、何を1件と数えるかを決めます。ここが曖昧なまま集計すると、後から数字が合わなくなります。

集計設計

基礎表で先に決める3つの単位

  • 年度の軸

    開始年度で数えます。1つの課題は、採択された年度の1行にだけ入ります。

  • 課題の種類

    研究課題(c6=project)に絞ります。領域や総括班は別の種別なので、混ぜません。

  • 部局の持ち主

    研究代表者の所属部局で数えます。採択時のデータには研究分担者の情報が含まれません。

開始年度で数える理由は、KAKENの年度指定に「実施期間の一部を含む」という条件があるためです。この条件で年度ごとに取ると、3年間の課題は3回出てきます。件数の表にこれを足すと、継続課題が毎年新規採択のように見えます。

APIを使うための準備

KAKEN APIの問い合わせ先は、研究課題が https://kaken.nii.ac.jp/opensearch/、研究者が https://nrid.nii.ac.jp/opensearch/ です。どちらもアプリケーションIDを appid= で渡します。

アプリケーションIDは、CiNiiウェブAPIのデベロッパー登録で取得します。CiNiiの登録ページには、この登録がKAKENと連携していて、取得したIDがKAKEN APIにも必要になると書かれています。

appidを付けずに呼ぶと、403 と Exceeds allowed rate を含むXMLが返ってきました。登録前に試すと、単なるアクセス過多に見えてしまいます。まずIDを取ります。

利用にあたっての約束も、登録ページにまとまっています。

  • 学術コンテンツサービス利用規程と、ウェブAPI利用細則に同意する
  • 短時間に大量のアクセスをしない。守られない場合は予告なく遮断や登録解除がある
  • 商用サイトで使う場合は、申請の前にCiNii側へ連絡する

後で触れるように、スクリプトにはページ間の待ち時間を入れます。学内向けの集計でも、アクセスの間隔は空けます。

採択課題を取るクエリ

使うパラメータは多くありません。KAKENのパラメータ資料(2026年3月版)から、この集計に要るものだけを抜き出します。

パラメータ内容この記事での値
qe内容研究機関で検索する。URLエンコードが必要この記事での値機関の正式名
s1 / s2内容助成期間のFrom / To。数字以外はエラーこの記事での値集計する年度の範囲
o1内容期間の条件。1が開始年度、3が実施期間の一部を含むこの記事での値1(既定値)
c6内容研究課題種別。project が研究課題この記事での値project
rw内容1ページの件数。20・50・100・200・500から選ぶこの記事での値500
st内容取得の開始番号。上限は200000件この記事での値1から500ずつ
format内容html5 か xml。既定は html5この記事での値xml
od内容並び順。3が研究開始年の古い順この記事での値3

format を省くと、通常の検索画面のHTMLが返ります。集計には xml を指定します。qe が受け取るのは機関名の文字列なので、「東京大学」のように正式名を渡します。

機関の統合があった場合は注意が要ります。KAKENのFAQには、詳細検索の研究機関に「筑波大学」と入れても、統合前の図書館情報大学の課題はヒットしないという説明があります。両方ほしいときは、図書館情報大学 OR 筑波大学 の形で入力します。qe も同じ研究機関の条件なので、統合や改称のある機関は、この形を試して件数を詳細検索の画面と突き合わせます。

Claude Codeに渡す指示と権限

スクリプトを書かせる前に、守らせたい点をCLAUDE.mdに置きます。次は、この集計用の断片の例です。

## KAKEN採択課題の集計
- 取得は `scripts/kaken_summary.py` だけで行う。appidは環境変数 KAKEN_APPID から読み、出力しない
- 件数は開始年度(o1=1)で数える。実施期間の一部を含む条件(o1=3)で取った行は足し合わせない
- 集計表を出したら、必ず `scripts/check_totals.py` で行数と合計の一致を確かめる
- 部局名・配分額の不整合は直さずに issues.csv へ出す。表の値を手で補正しない
- 配分額は採択時の額として扱い、表の注記にそう書く

権限は、IDの入ったファイルを読ませない方向で絞ります。権限規則はdeny、ask、allowの順に評価され、denyが優先されます。

{
  "permissions": {
    "allow": [
      "Bash(python3 scripts/kaken_summary.py *)",
      "Bash(python3 scripts/check_totals.py *)"
    ],
    "deny": ["Read(./.env)", "Read(./.env.*)"]
  }
}

Read のdenyが効くのは、Claudeの組み込みのファイルツールと、cat などBashのファイル読み出しです。PythonやNodeのスクリプトが自分でファイルを開く処理には効きません。appidはリポジトリの外に置き、スクリプトを起動するシェルにだけ渡す運用が確実です。

取得と集計のスクリプト

次のスクリプトは、機関名と年度の範囲を受け取り、500件ずつページを送って取得します。研究課題ごとに、研究種目・開始年度・配分額・研究代表者の所属部局を取り出し、部局別と種目別の表、点検表を書き出します。

要素名は、KAKENのXML定義資料にある階層に沿っています。研究課題は grantAward、最新の課題情報は summary、配分額の合計は overallAwardAmount の下の totalCost です。名前空間やラッパー要素の違いに左右されないよう、タグ名の末尾だけを見て探します。

"""KAKEN OpenSearch(研究課題をさがす, format=xml)から機関の採択課題を集め、集計表と点検表を出す。"""
import csv, os, sys, time, urllib.parse, urllib.request
import xml.etree.ElementTree as ET
from collections import defaultdict
 
BASE = os.environ.get("KAKEN_BASE", "https://kaken.nii.ac.jp/opensearch/")
XML_LANG = "{http://www.w3.org/XML/1998/namespace}lang"
PAGE = 500  # rw に指定できる最大値
 
 
def local(tag):
    return tag.rsplit("}", 1)[-1]
 
 
def kids(el, name):
    return [c for c in el if local(c.tag) == name]
 
 
def first(el, name):
    k = kids(el, name)
    return k[0] if k else None
 
 
def text(el):
    return " ".join("".join(el.itertext()).split()) if el is not None else ""
 
 
def fetch(inst, y1, y2, st):
    q = {"appid": os.environ["KAKEN_APPID"], "qe": inst, "s1": y1, "s2": y2,
         "o1": 1, "c6": "project", "od": 3, "rw": PAGE, "st": st, "format": "xml"}
    url = BASE + "?" + urllib.parse.urlencode(q)
    with urllib.request.urlopen(url, timeout=60) as r:
        return ET.fromstring(r.read())
 
 
def pick_summary(award):
    sums = kids(award, "summary")
    for s in sums:
        if s.get(XML_LANG) == "ja":
            return s
    return sums[0] if sums else None
 
 
def amount(summary):
    for oa in kids(summary, "overallAwardAmount"):
        if oa.get("planned") == "true":
            continue
        tc = first(oa, "totalCost")
        if tc is None:
            continue
        nv = first(tc, "normalizedValue")
        raw = text(nv) if nv is not None else text(tc)
        digits = "".join(ch for ch in raw if ch.isdigit())
        return int(digits) if digits else None
    return None
 
 
def parse(award):
    s = pick_summary(award)
    if s is None:
        return None
    pi = next((m for m in kids(s, "member") if m.get("role") == "principal_investigator"), None)
    dept, dept_code, pi_inst = "", "", ""
    if pi is not None:
        aff = first(pi, "affiliation")
        if aff is not None:
            d, i = first(aff, "department"), first(aff, "institution")
            dept, dept_code = text(d), (d.get("niiCode") or "") if d is not None else ""
            pi_inst = text(i)
    pa = first(s, "periodOfAward")
    sy = first(pa, "startFiscalYear") if pa is not None else None
    ps = first(s, "projectStatus")
    cats = kids(s, "category")
    return {
        "id": award.get("id", ""),
        "number": award.get("awardNumber", ""),
        "title": text(first(s, "title")),
        "category": text(cats[0]) if cats else "",
        "start_year": text(sy),
        "status": (ps.get("statusCode") if ps is not None else "") or "",
        "amount": amount(s),
        "dept": dept, "dept_code": dept_code, "pi_inst": pi_inst,
        "institutions": " / ".join(text(i) for i in kids(s, "institution")),
    }
 
 
def main(inst, y1, y2, out):
    rows, st = [], 1
    while True:
        root = fetch(inst, y1, y2, st)
        batch = [parse(a) for a in root.iter() if local(a.tag) == "grantAward"]
        batch = [b for b in batch if b]
        rows += batch
        if len(batch) < PAGE:
            break
        st += PAGE
        time.sleep(2)  # 短時間の大量アクセスを避ける
    os.makedirs(out, exist_ok=True)
    with open(f"{out}/projects.csv", "w", newline="", encoding="utf-8-sig") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0]) if rows else ["id"])
        w.writeheader(); w.writerows(rows)
 
    def pivot(key, name):
        cnt, amt = defaultdict(int), defaultdict(int)
        for r in rows:
            k = (r[key] or "(未設定)", r["start_year"])
            cnt[k] += 1; amt[k] += r["amount"] or 0
        years = sorted({k[1] for k in cnt})
        with open(f"{out}/{name}.csv", "w", newline="", encoding="utf-8-sig") as f:
            w = csv.writer(f)
            w.writerow([key] + [f"{y}件" for y in years] + [f"{y}円" for y in years])
            for g in sorted({k[0] for k in cnt}):
                w.writerow([g] + [cnt[(g, y)] for y in years] + [amt[(g, y)] for y in years])
 
    pivot("dept", "by_dept"); pivot("category", "by_category")
 
    issues = []
    ids = defaultdict(list)
    for r in rows:
        ids[r["id"]].append(r)
    issues += [("id重複", i, "") for i, v in ids.items() if len(v) > 1]
    by_code = defaultdict(set)
    for r in rows:
        if r["dept_code"]:
            by_code[r["dept_code"]].add(r["dept"])
    issues += [("部局表記ゆれ", c, " | ".join(sorted(v))) for c, v in by_code.items() if len(v) > 1]
    issues += [("部局コードなし", r["id"], r["dept"]) for r in rows if not r["dept_code"]]
    issues += [("配分額なし", r["id"], r["title"]) for r in rows if r["amount"] is None]
    issues += [("終了系ステータス", r["id"], r["status"]) for r in rows
               if r["status"] in ("declined", "discontinued")]
    issues += [("代表者所属が他機関", r["id"], r["pi_inst"]) for r in rows
               if r["pi_inst"] and inst not in r["pi_inst"]]
    with open(f"{out}/issues.csv", "w", newline="", encoding="utf-8-sig") as f:
        w = csv.writer(f); w.writerow(["種別", "課題ID/コード", "内容"]); w.writerows(issues)
    print(f"{len(rows)}件取得 / 点検 {len(issues)}件 -> {out}/")
 
 
if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])

点検用に、もう1本の短いスクリプトを置きます。部局別・種目別の表の合計が、課題一覧の行数と配分額の総和に一致するかを確かめます。

"""by_dept / by_category の件数・配分額の合計が projects.csv と一致するか確かめる。"""
import csv, sys
 
out = sys.argv[1]
rows = list(csv.DictReader(open(f"{out}/projects.csv", encoding="utf-8-sig")))
n, total = len(rows), sum(int(r["amount"] or 0) for r in rows)
for name in ("by_dept", "by_category"):
    t = list(csv.reader(open(f"{out}/{name}.csv", encoding="utf-8-sig")))
    half = (len(t[0]) - 1) // 2
    cnt = sum(int(v) for r in t[1:] for v in r[1:1 + half])
    amt = sum(int(v) for r in t[1:] for v in r[1 + half:])
    ok = "OK" if (cnt, amt) == (n, total) else "NG"
    print(f"{name}: {cnt}件 {amt}円 / projects.csv {n}件 {total}円 {ok}")

実行は次のとおりです。IDは環境変数で渡し、出力先は集計のたびに分けます。

export KAKEN_APPID="<取得したアプリケーションID>"
python3 scripts/kaken_summary.py 東京大学 2022 2026 out/2026
python3 scripts/check_totals.py out/2026

check_totals.py が NG を返したら、表を出し直す前に原因を見ます。部局が空の行や配分額のない行があっても、行数は一致します。ずれるのは、課題が表のどこにも入っていないときです。

最初の1回で実際の応答と突き合わせる

スクリプトの要素名は定義資料に合わせて書いたものです。年度全体を回す前に、1ページ分の応答を保存し、要素の並びが想定どおりかをClaude Codeに読ませます。

rw=20 の1ページ分のXMLを out/sample.xml に保存して。
grantAward ごとに、種目・開始年度・配分額・研究代表者の所属部局が
kaken_summary.py の parse() で取り出せているか、5件で示して。
取り出せない項目があれば、実際の要素名を挙げて parse() を直す案だけ出して。

これは依頼文の例です。応答が想定と違っていても、この段階なら直すのは parse() の数行で済みます。全年度を取ってから気づくと、アクセスをやり直すことになります。

二重計上を点検する

課題一覧が正しくても、足し方で数字は崩れます。点検するのは次の4点です。

開始年度以外で取っていないか

o1=3 や o1=4 で年度ごとに取ると、同じ課題が複数の年度に出ます。issues.csv の「id重複」は、この混入をIDの重複で拾います。表の合計が一致していても、重複があれば件数は水増しです。

配分額を実際の支出と読んでいないか

詳細画面の説明では、「配分額」の総額は研究期間全体の交付予定額です。繰越しや調整金制度による変更、留保・中断は反映されません。間接経費は、直接経費の30%を一律で載せています。

2015年以前の採択課題は扱いが違い、各年度の額は採択当初の交付内定額で、総額と各年度の合計が一致しないことがあります。年度別の推移表には、この古い年度を混ぜる前に注記を入れます。

辞退・中途終了の課題をどう扱うか

課題のステータスには、採択後辞退と中途終了があります。辞退・廃止された課題は、研究期間だけが更新され、他の項目は変わりません。件数にも配分額にも残るので、issues.csv の「終了系ステータス」で数を見て、含めるか外すかを表の注記に書きます。

共同機関の課題を拾っていないか

qe は機関で検索するので、他機関の代表者の課題に分担機関として載っている場合も含まれ得ます。「代表者所属が他機関」の行は、自機関の代表課題として数えるかを方針で決めます。

部局名の表記ゆれをそろえる

部局は課題ではなく、研究者の所属に付いた情報です。XML定義では、研究者の所属(affiliation)の下に機関と部局があり、部局には niiCode が付きます。

同じ部局が「理学系研究科」と「理学研究科」のように違う文字列で入っていても、コードが同じなら同一とみなせます。スクリプトは、同じコードに複数の表記がある場合を「部局表記ゆれ」として出し、コードのない行を「部局コードなし」として出します。

対応表は、点検表を読んだClaude Codeに作らせます。

out/2026/issues.csv の「部局表記ゆれ」と「部局コードなし」を読んで、
scripts/dept_map.csv(列: 元の表記, 集計用の部局名)の案を作って。
コードが同じ表記は1つにまとめ、コードのない行は推測で割り当てず、
「要確認」として別の列に残して。

コードのない行を推測で埋めさせない点が大事です。どの部局に当たるかは、学内の組織表を持つ人が決めます。対応表ができたら、kaken_summary.py に読み込ませて表を作り直し、check_totals.py を再実行します。

年度の途中は数字が動く

KAKENの更新には年間の予定があります。4月中旬から下旬に交付内定データ(採択課題)、7月に交付決定データ、11月から12月に実施状況報告書・実績報告書のデータで更新されます。

トップページには、その年度の採択課題情報を「一部」収録したという告知が出ることがあります。年度の途中に取った表は、その時点の収録分です。完成値として配布する表は、採択の更新が落ち着いてから作り直します。

採択課題のデータは1965年から収録されています。ただし1995年以前は冊子が元になっていて、追加採用された課題には収録されていないものがあります。長い期間の推移表は、古い年度の欠けを前提に読みます。

出典と加工の書き方

KAKENのコンテンツは、一部の報告書を除いて文部科学省ウェブサイト利用規約に準拠していて、CC BYと互換性があると案内されています。利用時は出典を書きます。

編集・加工した表を出すときは、出典とは別に加工したことを書きます。たとえば次のとおりです。

出典:KAKEN:科学研究費助成事業データベース(国立情報学研究所)
(https://kaken.nii.ac.jp/)をもとに〇〇大学作成

集計表を学内外に出すときは、この記載に加えて、配分額が採択時の額であること、集計の単位(開始年度・研究課題)を表の下に書き添えます。NIIが作成した数字のように見える書き方は避けます。

KAKENのデータには誤りが含まれることがあり、気づいた点は問い合わせ窓口に連絡すると案内されています。表の数字が大学の公式な採択件数と食い違うときも、まず取得条件と数え方の差から見ます。

研究者側の業績や税制の集計との使い分け

同じ研究IRでも、見る対象で入口が変わります。研究者の業績をORCIDから取って一覧にする手順は、Claude CodeでORCID APIの業績を取り込む記事にあります。申請書の側から科研費を扱うなら、研究計画調書の点検が対応します。

CSVに集めてから金額を試算する流れは、研究開発税制の試験研究費の集計と共通です。いずれも、数え方を先に決め、点検表を出させてから表にする順序が効きます。

まとめ

基礎表の作り方は、取得条件を固定し、点検表を出し、対応表で補正してから集計する流れです。

  • 年度は開始年度(o1=1)、課題は研究課題(c6=project)、部局は研究代表者の所属で数える
  • 合計の一致は check_totals.py で、重複と表記ゆれは issues.csv で確かめる
  • 配分額は採択時の額で、2015年以前は総額と年度合計がずれうる。表の注記に書く
  • 公開前に出典と加工の記載を付け、年度途中の表は更新後に作り直す

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. support.nii.ac.jp/ja/kaken/api/api_outline
  2. bitbucket.org/niijp/kaken_definition
  3. support.nii.ac.jp/ja/kaken/about/datatype
  4. support.nii.ac.jp/ja/kaken/project_details
  5. support.nii.ac.jp/ja/cinii/api/developer
  6. support.nii.ac.jp/ja/kaken/about/terms
  7. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn