個人開発でCursorを本格導入したらコード生産性が3倍になった

# はじめに
直近半年（育休中）の個人開発のデータを用いて、2月にCursor Agentを本格的に使いだしたことの効果を~~自腹を切ってよかったと思い込むために~~検証してみました。

比較対象は「Cursorのcmd+Kやチャット機能を中心に使っていた時期」と「追加でエージェント機能を本格的に使い始めてからの時期」です。コーディングの相談をAIにすることはすでに一般的になっていると思いますが、追加でclineやwindsurf、Cursorなどの、より作り込まれたAIコーディングエディタに課金するべきか考え中の方の参考になればと思います。

# 開発したもの
生成AI、音韻・音声処理、CI/CDなどその時時で興味のあるものを気ままに開発していました（[詳細](https://qiita.com/shimajiroxyz/private/b0bb5a95d1678e3f316c)）。

言語は主にpythonです。

# Cursor利用状況
Cursor自体は無料プランでcmd+Kとチャットを中心に1年以上使っていました。
育休開始の10月からProプランを契約し、コード補完も使うようになりました。Composerは出始めの頃に触ったのですが、うまく使いこなせず、その印象が残り続けてほとんど使っていませんでした。
2月にComposerがAgent機能に一本化されてから再度使うようになりました。

Agent機能モデルは主にdefaultを使っていました。マークダウンの生成にgpt-4oを用いたり、defaultで詰まった問題に対してcalude 3.7 sonnetを使うこともありました。ただdefaultで詰まった場合、モデルを変えても引き続き解決しないことが多かったのでそこまで積極的には使っていません。

Cursorの使いこなし度合いは、SNSでよく目にするような方々などと比べると低いと思います。ルールファイルはuvを使用することなどCursorとやり取りする中でこちらから指示することが多かった内容を都度更新はしていますが、あまり丁寧には作っていません。

費用はProプランの20ドル/月が半年分と、3月に従量課金でもう20ドルくらいの、合計で140ドルくらい支払いました。

本記事では2月にAgent機能を積極的に使い始めたタイミングを「Cursorの本格導入」とし、この前後で、コード生産性を比較することで、Cursor本格導入の効果を検証してみます。つまり、生成AIを一切使わなったときとの比較ではなく、Cursorのエージェント以外の機能を使っていたときと、エージェント機能を使い始めてからの比較です。

# 方法
## コード生産性の指標
GitHubのプルリクエスト（PR）数とQiita記事の投稿数をメインの指標とします。

GitHubのPR数はコード生産性のほとんど直接的な指標と考えられます。厳密には、時期によってPRあたりの規模が異なっていたりすると比較に使えない可能性がありますが、今回は個人開発ですので、PRの作成基準は大きく変わっていないと仮定します。

Qiitaの投稿数は直接的な指標ではありませんが、何らかのコードの解説に関する記事がほとんどであるため、コード生産性が高まれば間接的に恩恵を受けると考えました。また、文章の生成や校正などにがCursorによって効率化される可能性もあると考えました。

## PR数や投稿数の取得
GitHub APIおよびQiita APIを用いてPR数、投稿数を取得しました。
GitHubについては、筆者のリポジトリを集計対象とするプログラムを実行してから、他者のリポジトリに対するPRについては目視でカウントし補正をしました。なおこの値は、GitHubのホームページで確認できる値と1、2個異なることがありましたが、差が大きすぎないこととGitHubホームページの集計ロジックが不明であることから、今回はプログラムによる値を使いました。

## 比較期間
月ごとのPR数と投稿数を集計したうえで、育休初期（2024年10、11月）と育休後期（2025年2、3月）の値を比較しました。
育休中期（2024年12月-2025年1月）は年末年始や体調不良によりCursorとは無関係な理由であまり稼働できなかったため、比較対象から除きました。
また育休前期は、子どもの睡眠が長く開発時間を確保しやすかっため、大きい値が出やすい可能性があります。
育休後期は、Cursorを末日が集計プログラム実行日の3/28であることと本格導入したのが2月中旬であることとから、本格導入の効果が若干小さく見える可能性があります。

# 結果
月ごとのPR数、投稿数は下図の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259353/24f31f0a-958c-44e1-8d7e-c39896b8e908.png)

PR数は育休前期、後期でそれぞれ13件、42件、投稿数はそれぞれ27件、22件でした。

# 考察

## PR数
PR数は13件から42件と大幅（3.2倍）に上昇していました。
育休前期が有利、育休後期が不利な条件であることを踏まえると、Cursorの効果は今回見えている以上に大きい可能性もあります。
育休後期のなかでは2月（9件）より3月（33件）のほうがよりPR数が大きいです。これはCursorの本格導入が2月中旬だったことと、コミットメッセージの自動生成やPRの自動生成のやり方に3月に入ってから気づくなど、Cursor Agentの使い方に徐々に習熟したためと考えられます。

この数値上の上昇は体感とも合致しており、エージェント機能を使う前はベースとなるコードはほとんど自分で書いてから部分的にAIにさせることが多かったのですが、使い始めてからはエージェントが自律的に関連ファイルを参照できるため、コードのドラフトの生成品質が高まり、自分でコーディングする量が減った印象がありました。
なお3月はPRの文章もAgentに考えさせていたため、PRを作成するめんどくささが減ったことで、それまではまとめて出していたPRを、小分けにするようになった可能性があります。ただし変更、追加されたコードの行数で比較しても育休前期と後期で同様の上昇が見られたため、単にPRが小分けになっただけではないと考えられます。

## 投稿数
PR数は増加した一方、Qiitaの投稿数は予想に反して減少しました（27 -> 22件）。
理由の一つは10月の投稿数が19と非常に多かったことです。このころはまだ子どもの睡眠時間が短かったので、初月のやる気とも合わせて、たくさん記事を書けたと思われます。
もう一つの理由は、文章生成に対してはコードよりも高い品質を筆者が無意識に求めてしまい、Cursorエージェントの提案をほとんど採用できなかったためです。コードは動けばあまりこだわりはないのですが、文章は論理のつながりやストーリー構成で意にそぐわない生成をされることが多かったので、下書きは筆者がほぼ全て書くことが多かったです。このため、記事の元になるコードは早く完成するなどの間接的な恩恵はあったものの、文章生成においては、初月のやる気と時間を上回るほどの効果が見られなかったと考えられます。一方で、[Cursorでブログ等の執筆を効率化させた事例](https://honeshabri.hatenablog.com/entry/cursor_markdown_ecosystem)も存在するため、筆者の使いこなし度合いによってこの問題は解決可能かもしれません。ルールファイルの整備などしてみてもっと効率を高められるかは今後の課題とします。


## 制限事項
PR数やコードの追加行数がコード生産性の良い指標であるかは、慎重な議論が必要です。
今回は開発者兼評価者が1人でありハックするメリットも少ないのである程度使えると思って使いましたが、チーム開発などをしている状況であれば、より多面的に評価したほうがいいと思います。

比較において、Cursorの本格導入以外に、開発者の体調、やる気、育児状況などがコード生産性に影響した可能性があります。例えば、育休初期は子どもの月齢が小さいことで睡眠時間が長かったため、開発時間を長く取りやすかった可能性が高いです。また育休後期では、学会ワークショップでの発表があったため、締切効果で開発が進捗した可能性もあれば、プレゼン資料や調査も必要だったためコーディングの時間は減っていた可能性があります。ただし、個人的な感覚としては、これらの要因はトータルで見ると相殺されるかむしろ育休初期に有利に働く可能性が高いため、Cursorの本格導入によってコード生産性が向上したという示唆自体はある程度信頼できるのではと思っています。

# おわりに
直近半年間の開発成果を活用して、Cursorの本格導入の効果を検証してみました。
体感的にはかなり便利だったのですが、それが数値でも確認できてよかったです。
また文章生成に対する恩恵は、コード生成に対する恩恵よりは少なそうだという結果も興味深かったです。ルールファイルなど改善の余地はいろいろ残されているので、また試してみたいです。

# 付録
## 実働時間
育休中の実働時間は半年でおよそ540hと推定されました。
macのスクリーンタイムで直近1ヶ月（3月）のCursorなどの使用時間を計算したところ、2.5h/日でした。
ブラウザで調べ物をしていた時間は上記に含まれないため仮に30分くらいとすると、3h/日くらいが開発時間と考えられます。体感的には時期や日によりますが、2-4h/日くらい（主に子どもの睡眠中）だったので、それとも大幅にズレておらず、そんなものかなと思います。

投稿数とPR数の合計が119のため、4時間半の実働で1つアウトプットしていたくらいの感じです。
一つのタスクは半日以内に終われるくらいの粒度を目指していたので、割といい感じかなと思います。

## サブ指標
メイン指標以外にもいくつか数値を取得していたのでこちらに記載します。

2024-10-01から2025-3-28を集計期間とし、Qiita APIやGitHub APIで必要な数値を取得しました。
なお、GitHubのPR数とコミット数は、筆者が作成したリポジトリが集計対象となるプログラムで取得した後、他者のリポジトリに対するPR数（11月に1回、12月に3回）、コミット数（11月に3回、12月に10回）を目視で確認し加算しました。

### Qiita


| 月       | 投稿数 | いいね数 | ストック数 | リアクション数<br>（いいね＋ストック） | 閲覧数   |
|----------|--------|----------|------------|------------------------------|----------|
| 2024-10  | 19     | 13       | 4          | 17                           | 15,934   |
| 2024-11  | 8      | 9        | 1          | 10                           | 6,956    |
| 2024-12  | 6      | 3        | 3          | 6                            | 3,947    |
| 2025-01  | 6      | 2        | 3          | 5                            | 4,705    |
| 2025-02  | 5      | 1        | 0          | 1                            | 1,938    |
| 2025-03  | 15     | 2        | 1          | 3                            | 2,522    |
| **合計** | **59** | **30**   | **12**     | **42**                       | **36,002** |
### GitHub

| 月       | マージ済み<br>PR数 | コミット数 | 追加行数 | 削除行数 |
|----------|----------------|------------|------------------------|------------------------|
| 2024-10  | 9              | 181        | 5,932                  | 5,229                  |
| 2024-11  | 4              | 16         | 1,564                  | 256                    |
| 2024-12  | 4              | 13          | 90                     | 0                      |
| 2025-01  | 0              | 4          | 168                    | 1                      |
| 2025-02  | 9              | 73         | 13,032                 | 5,256                  |
| 2025-03  | 33             | 187        | 13,705                 | 3,866                  |
| **合計** | **59**         | **462**    | **34,491**             | **14,608**             |

## Qiitaのリアクション数や閲覧数の絶対評価
閲覧数は半年間で約36000でした。有名な方などに比べると非常に僅かな数値かと思いますが、絶対値としてはたくさん見てもらえたんだなあと割と満足しています。
リアクション数は投稿数あたりで見ると1を切っているので、少し粗製乱造気味だったかもと思わなくもないですが、とはいえQiitaの記事は筆者のアカウント程度の知名度（ほぼ0）ではスタートダッシュがないのが当たり前なので、長期的な目で見ることをまだ許される範囲かとも思います。

自分に甘いかもしれません。

## PRとコードの追加行数の関係
育休後期にPR数が増えたという結果でしたが、PRの粒度を細かくして指標をハックしていないか気になるので、コードの追加行数も含めて検討してみます。コードの削除数は、期間中は新規のものを作ることが多かったので、今回は検討しません。

各月の1PRあたりのコード追加行数を求めると、10、11、2、3月はそれぞれ659、391、1448、415でした。
2月の1448が若干外れ値になっていますが、33件のPRがあった3月単月でみたとしても、10、11月に対して、極端に小さくなっていることはなさそうです。

ちなみに2月はthreadingで書いていた音声対話システムの処理をasyncに置き換えるような作業をしていたので、比較的大規模なPRが多かったと思われます。

## 集計コード

### Qiita

```Python
import datetime
from collections import defaultdict

import requests
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    qiita_token: str = Field(..., description="Qiitaのアクセストークン")
    start_month: str = Field(default="1900-01", description="合計期間の開始月")
    end_month: str = Field(default="2100-01", description="合計期間の終了月")

    model_config = SettingsConfigDict(
        env_file=".env",
        case_sensitive=False,
        cli_parse_args=True,
        extra="allow",
    )


def main():
    # 設定の読み込み
    settings = Settings()

    # ここにQiitaのアクセストークンを設定
    ACCESS_TOKEN = settings.qiita_token

    headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}

    # 認証済みユーザーの投稿を取得するエンドポイント
    url = "https://qiita.com/api/v2/authenticated_user/items"

    items = []
    page = 1
    per_page = 100  # 1ページあたりの取得件数

    # ページネーションに対応してすべての投稿を取得する
    while True:
        params = {"page": page, "per_page": per_page}
        resp = requests.get(url, headers=headers, params=params)
        if resp.status_code != 200:
            print("Error:", resp.text)
            break
        data = resp.json()
        if not data:  # 取得データがなくなったらループ終了
            break
        items.extend(data)
        page += 1

    # 月ごとの投稿数とリアクション数を集計する
    monthly_stats = defaultdict(
        lambda: {
            "post_count": 0,
            "likes_count": 0,
            "stocks_count": 0,
            "page_views_count": 0,
        }
    )
    for item in items:
        created_at = item.get("created_at")
        # 日時文字列をdatetimeオブジェクトに変換
        dt = datetime.datetime.strptime(created_at, "%Y-%m-%dT%H:%M:%S%z")
        month_str = dt.strftime("%Y-%m")
        if settings.start_month <= month_str <= settings.end_month:
            monthly_stats[month_str]["post_count"] += 1
            monthly_stats[month_str]["likes_count"] += item.get("likes_count", 0)
            monthly_stats[month_str]["stocks_count"] += item.get("stocks_count", 0)
            monthly_stats[month_str]["page_views_count"] += item.get(
                "page_views_count", 0
            )

    # 結果の出力
    for month, stats in sorted(monthly_stats.items()):
        print(
            f"{month}: 投稿数: {stats['post_count']}, いいね数: {stats['likes_count']}, ストック数: {stats['stocks_count']}, 閲覧数: {stats['page_views_count']}"
        )

    # 指定された期間の合計を計算
    total_posts = sum(
        monthly_stats[m]["post_count"]
        for m in monthly_stats
        if settings.start_month <= m <= settings.end_month
    )
    total_reactions = sum(
        monthly_stats[m]["likes_count"] + monthly_stats[m]["stocks_count"]
        for m in monthly_stats
        if settings.start_month <= m <= settings.end_month
    )
    total_page_views = sum(
        monthly_stats[m]["page_views_count"]
        for m in monthly_stats
        if settings.start_month <= m <= settings.end_month
    )
    print(f"\n{settings.start_month}から{settings.end_month}の合計:")
    print(f"投稿数: {total_posts}")
    print(f"リアクション数: {total_reactions}")
    print(f"閲覧数: {total_page_views}")


if __name__ == "__main__":
    main()
```

```sh
% uv run qiita_count.py --start_month 2024-10 --end_month 2025-03
2024-10: 投稿数: 19, いいね数: 13, ストック数: 4, 閲覧数: 15934
2024-11: 投稿数: 8, いいね数: 9, ストック数: 1, 閲覧数: 6956
2024-12: 投稿数: 6, いいね数: 3, ストック数: 3, 閲覧数: 3947
2025-01: 投稿数: 6, いいね数: 2, ストック数: 3, 閲覧数: 4705
2025-02: 投稿数: 5, いいね数: 1, ストック数: 0, 閲覧数: 1938
2025-03: 投稿数: 15, いいね数: 2, ストック数: 1, 閲覧数: 2522

2024-10から2025-03の合計:
投稿数: 59
リアクション数: 42
閲覧数: 36002
```
### GitHub
```Python
import calendar
import os
from collections import defaultdict
from datetime import datetime

import pandas as pd
from github import Github
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from tqdm import tqdm


# 環境変数から設定を取得するクラス
class Settings(BaseSettings):
    GITHUB_TOKEN: str = Field(
        default="",
        description="GitHubのPersonal Access Token。ContentsとPull Requestsのread権限が必要",
    )
    username: str = Field(default="", description="GitHubのユーザー名")
    start_month: str = Field(default="1900-01", description="合計期間の開始月")
    end_month: str = Field(default="2100-01", description="合計期間の終了月")

    model_config = SettingsConfigDict(
        env_file=".env",
        case_sensitive=False,
        cli_parse_args=True,
        extra="allow",
    )


# 設定を読み込む
def main():
    settings = Settings()
    print(settings)

    # GitHub APIの初期化
    GITHUB_TOKEN = settings.GITHUB_TOKEN
    USERNAME = settings.username
    # 合計期間の開始月と終了月
    start_month = settings.start_month
    end_month = settings.end_month

    g = Github(GITHUB_TOKEN)
    rate_limit = g.get_rate_limit()
    print(f"API Rate Limit: {rate_limit.core.remaining}/{rate_limit.core.limit}")

    user = g.get_user(USERNAME)

    # 各月ごとのコミット数とマージされたPR数を保持する辞書
    commit_data = defaultdict(int)
    pr_data = defaultdict(int)
    # 各月ごとの追加・削除行数を保持する辞書
    additions_data = defaultdict(int)
    deletions_data = defaultdict(int)

    # 対象とするファイル拡張子
    target_extensions = {".py"}

    print("=== 処理開始 ===")
    # ユーザー所有の各リポジトリに対して処理を行う
    for repo in tqdm(user.get_repos()):
        # 合計期間の開始月と終了月をdatetime型に変換
        start_month_dt = datetime.strptime(start_month, "%Y-%m")
        # end_monthを月の最終日に設定
        end_year, end_month_num = map(int, end_month.split("-"))
        last_day = calendar.monthrange(end_year, end_month_num)[1]
        end_month_dt = datetime(end_year, end_month_num, last_day)

        # 自分が作成したコミットを取得（特定の期間に制限）
        commits = repo.get_commits(since=start_month_dt, until=end_month_dt)
        for commit in commits:
            commit_date = commit.commit.author.date  # コミット日時
            month_str = commit_date.strftime("%Y-%m")
            commit_data[month_str] += 1

            # コミットの変更ファイルを取得
            for file in commit.files:
                # ファイルの拡張子を取得
                _, ext = os.path.splitext(file.filename)
                # 対象の拡張子の場合のみ集計
                if ext in target_extensions:
                    additions_data[month_str] += file.additions
                    deletions_data[month_str] += file.deletions

        
        # 閉じたプルリクエストを取得
        pulls = repo.get_pulls(state="closed")
        for pr in pulls:
            closed_date = pr.closed_at
            if closed_date is not None:
                month_str = closed_date.strftime("%Y-%m")
                pr_data[month_str] += 1

    # 辞書のデータを pandas DataFrame に変換し、月ごとにソート
    commit_df = pd.DataFrame(
        list(commit_data.items()), columns=["Month", "Commits"]
    ).sort_values("Month")
    pr_df = pd.DataFrame(
        list(pr_data.items()), columns=["Month", "Merged PRs"]
    ).sort_values("Month")
    additions_df = pd.DataFrame(
        list(additions_data.items()), columns=["Month", "Additions"]
    ).sort_values("Month")
    deletions_df = pd.DataFrame(
        list(deletions_data.items()), columns=["Month", "Deletions"]
    ).sort_values("Month")

    print("=== コミット数（月ごと） ===")
    print(commit_df)
    print("\n=== PR数（月ごと） ===")
    print(pr_df)
    print("\n=== 追加行数（月ごと）[.py] ===")
    print(additions_df)
    print("\n=== 削除行数（月ごと）[.py] ===")
    print(deletions_df)

    # 月ごとのデータをCSVに出力
    commit_df.to_csv("results/monthly_commits.csv", index=False)
    pr_df.to_csv("results/monthly_prs.csv", index=False)
    additions_df.to_csv("results/monthly_additions.csv", index=False)
    deletions_df.to_csv("results/monthly_deletions.csv", index=False)

    # コミット数の合計を計算
    total_commits = commit_df[
        (commit_df["Month"] >= start_month) & (commit_df["Month"] <= end_month)
    ]["Commits"].sum()

    # PR数の合計を計算
    total_prs = pr_df[(pr_df["Month"] >= start_month) & (pr_df["Month"] <= end_month)][
        "Merged PRs"
    ].sum()

    # 追加・削除行数の合計を計算
    total_additions = additions_df[
        (additions_df["Month"] >= start_month) & (additions_df["Month"] <= end_month)
    ]["Additions"].sum()
    total_deletions = deletions_df[
        (deletions_df["Month"] >= start_month) & (deletions_df["Month"] <= end_month)
    ]["Deletions"].sum()

    print(f"\n=== 期間({start_month}-{end_month})の合計 ===")
    print(f"コミット数合計: {total_commits}")
    print(f"PR数合計: {total_prs}")
    print(f"追加行数合計[.py]: {total_additions}")
    print(f"削除行数合計[.py]: {total_deletions}")
    print(f"純増加行数[.py]: {total_additions - total_deletions}")


if __name__ == "__main__":
    main()
```

```sh
% uv run git_count.py --start_month 2024-10 --end_month 2025-03 --username USERNAME
=== 処理終了 ===
=== コミット数（月ごと） ===
     Month  Commits
0  2024-10      181
3  2024-11       14
5  2024-12        3
4  2025-01        4
2  2025-02       73
1  2025-03      187

=== PR数（月ごと） ===
     Month  Merged PRs
4  2024-05           2
0  2024-10           9
3  2024-11           3
5  2024-12           1
2  2025-02           9
1  2025-03          33

=== 追加行数（月ごと）[.py] ===
     Month  Additions
0  2024-10       5932
3  2024-11       1564
5  2024-12         90
4  2025-01        168
2  2025-02      13032
1  2025-03      13705

=== 削除行数（月ごと）[.py] ===
     Month  Deletions
0  2024-10       5229
3  2024-11        256
5  2024-12          0
4  2025-01          1
2  2025-02       5256
1  2025-03       3866

=== 期間(2024-10-2025-03)の合計 ===
コミット数合計: 462
PR数合計: 55
追加行数合計[.py]: 34491
削除行数合計[.py]: 14608
純増加行数[.py]: 19883
```