21
3

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はAtCoderBeginnerContestを実際に破壊しているのか

Last updated at Posted at 2025-03-04

最近AtCoderBeginnerContest(ABC)破壊ひどくないか…

アマチュアプログラマ歴10年(医師歴9年)でQiitaの初投稿がAtCoderにおける生成AIの影響分析になるとは。
先回りしますが、結論はこの図です。
Figure_2.png

要約すると

現在寒色の古参ユーザーはAI使用者の出現により、順当なレート上昇および学習到達度の把握を阻害されている可能性が否めない。

具体的には、ChatGPT o1,o3リリース後1,2ヶ月間のABCにおけるレート変化量(コンテスト成績表ページにおける"差分"の項)が、1年前の同時期と比較し平均5程度減少しており、パフォーマンス(レート)表示に換算すると本来の実力と比較して50程度低く評価されていた可能性がある。

です。

生成AIに対する筆者のスタンスおよび本分析研究のきっかけ

[著者略歴]

精神科医、博士。市中病院で臨床やりつつ協力研究員として脳波研究の主にプログラミング部分を担当。医学部卒業半年前に休学し1年間独学でプログラミングを勉強、その後もwebアプリや研究用のスクリプトを個人的に作っては仲間内で細々と使っています。2022年春にセキスペ合格(FE,APも合格済)。AtCoderは2022年2月に始めて、ABCのdifficulty2200以下問題は8割以上ほぼ2周解いていますが未だレートは1500程度しかありません。職業柄もありPythonユーザー。業務中に問題を解いているように見える提出履歴がありますが、私には「研究遂行に不可欠なプログラミング能力を伸ばす」という大義名分があり、これは裁量労働制における今流行りの自己研鑽の時間なのです!オダマリ!
スクリーンショット 2025-03-03 143023.png

略歴にあります通り、才能に乏しいなりにも精進の甲斐あって、私のレート推移は2年半前から線形性を保って緩やかに上昇しているので、これは生成AIへの怨念で書いた記事では無いことをあらかじめご了承ください。また、一番好きな技術書はオライリーの「コンピュータシステムの理論と実装」ではありますが、本分析にあたっても無課金GPTの力を大いに借りるなど、基本的にはアンチAIの立場ではありません
競技プログラミングにおける生成AI使用に対しての私自身のスタンスは「レートに一定の社会的価値があるとされている現状では、隠れて生成AIを使用する人間が発生する事態は仕方が無いと考える反面、虚栄のためだけに学びも面白さも達成感も殆ど無いAI使用メインでのコンテスト参加で時間を浪費している人間のことは全く理解できず心底軽蔑する。ただ医師である私自身のAtCoderレートに社会的価値は殆ど無くそのため毎週末冷えた温まったに一喜一憂することは基本的に無いため、隠れAI使用者増加による弊害は私個人にとっては"順位やAtCoderProblemsのdifficulty評価が到達度評価の参考値として機能しなくなって若干不便"程度のものであり、AI使用者の増加を憂う時間があるなら新しいアルゴリズムやデータ構造、そしてその応用方法を一つでも多く理解して脳汁をより多く出したい。」です。クソデカ感情乙
ただ、最近のABC順位破壊は目に余るものがあるとも感じており、難易度や到達度の把握が難しく感じる機会が増えて若干の不都合さを感じるようになったため、研究者の性もあり、生成AIのABCに対する潜在的影響が如何程のものか無性に調査したくなりGPTと相談して分析してみました。その結果あまりに見事に仮説検証が成功したため、感動のあまり論文を書いてしまいました

お待たせしました。それでは暇人PhDと無課金GPTの夢のコラボレーションをお楽しみください

※今回の分析にあたり、約1000人のユーザーのコンテスト成績表ページに1回/secでrequestを送りスクレイピングを行いました(つまり約15分かけてGETを約1000回)。統計学的に十分なサンプル数となりうる範囲で、負荷としては小さいものになるよう極力工夫したつもりです。AtCoder社様お許しください。

以下、論文の体裁で記載しています。読み慣れない人はGPTに流し込んで解説してもらってください。逆に統計に詳しい人などは追加検証や問題点の指摘をしていただけると勉強になります。なお、誓って記載以上の多重比較は行っておりませんので、統計学的有意差は本物です(効果量含め結果に意味があるかどうかは皆さんの考察に委ねます)

Title : ChatGPTの発展がAtCoder中級ユーザーのAtCoderBeginnerContestにおけるレート変化量に与えた影響について

著者:Daiki Soma (MD)
データ:AtCoderウェブサイトにおけるランキングおよび個別ユーザーコンテスト成績表(例:https://atcoder.jp/users/sirsoldano/history
利益相反:開示すべきCOIはありません。

Abstract

【Introduction】競技プログラミング界においても生成AIの進歩は目覚ましい。オンラインコンテストにおけるAI使用はその感知が困難であり、レートの形骸化、順位や難易度目安を通した実力並びにその推移の把握が難しくなる事などが懸念される。今回私はAtCoderBeginnerContest(以下ABC)における生成AIの潜在的な利用がAtCoder長期間レート安定中級ユーザーのレート変化量(コンテスト成績表ページにおける"差分"の項)に与える影響を分析した。
【Method】 [ 2024年3月2日時点でレートが1000以上2000未満 ] かつ [ コンテスト出場回数が100回以上 ] かつ [ 2023年1月1日以降のABC終了時点でのレートが1000未満となったことが一度も無い ] 総計938人について、コントロール2期間(2023/9-10, 2024/1-2)および生成AI関連の影響が大きいと考えられる4期間(4o, o1, AI禁止令後, o3)におけるABCでのレート変化量を観察、比較した。一期間あたりのコンテスト回数は8回(o3期のみ5回)として設定し、各期間の出場回数が一度でも3回に満たないユーザーは除外した。最終的に235人、データ総数9045件について、Paired t-testを用いて各期間同士のレート変化量平均の差が有意であるか検定を行った(多重比較補正のため有意水準をp<0.01とした)。
【Result】2024年1-2月と比較して、o1期、禁止期、o3期においてレート変化量が有意に減少しており、とりわけo1期(t=4.953,p<0.001,95%CI 2.845-6.603)、o3期(t=4.179,p<0.001,95%CI 2.457-6.841)でその傾向が顕著であった。
【Discussion】長くAtCoderを利用しておりかつレートが1000〜2000程度で2年間安定しているユーザーにおいて、o1以降の生成AIのリリース時期に合わせて、ABCにおけるレート変化量が有意に減少していたことが確認された。また、AtCoder社によるAI禁止令には一定の効果があった可能性がある。今後のリリース、claudeやdeepseekなどその他生成AIの影響、他レート帯への影響などを今後も注視していく必要があると同時に、中下級ユーザー側にもレートに固執しない姿勢が求められる。

Introduction

chatGPTをはじめとする生成AIの能力向上は目覚ましく、競技プログラミング界においても昨年のopenAI o1-preview(以下o1)以降、典型問題を中心にdifficulty(AtCoder Problemsにおける難易度目安)2000強の問題まで生成AI独力で解くことができるようになっている。

image.png
(画像1 : 生成AIによる初のABC大破壊回として伝説に残るABC377)

日本最大の競技プログラミングコンテストサイトであるAtCoderでは毎週末、競技プログラミングにおける典型問題を中心に100分間で何問解くことができるか、その速さやミスの少なさを競うオンラインコンテストAtCoder Beginner Contest(以下ABC)が開催されている。o1の出現およびABCの制覇(画像1)により2024年11月15日以降、ABCにおける生成AIの利用は翻訳および言語変換のみに制限されている(生成AIの技術向上に伴うABCおよびARCにおけるルール変更について)。o1は無料使用できなかったが、2025年1月31日に発表されたopenAI o3-mini(以下o3)は無料で使用することができ、現在ABCでは使用が制限されているにも関わらず堂々とあるいは陰で生成AIを不正利用していると推測される事象が多数観測されている。

生成AIの登場によりとりわけ2000未満の低~中レート帯において、既にオンラインコンテストにおけるレート推移のみから客観的な実力を把握することは困難となりつつあるが、同時に個人が自身の実力の推移を正確に把握することを難しくしている側面も、参加者のモチベーション維持の観点等から問題となりうる。

今回私は、ABCにおける生成AIの潜在的な利用がAtCoder長期利用者のレート変化量(コンテスト成績表ページにおける"差分"の項)に与える影響の大きさを、特定の6期間について各期間ごとのレート変化量を観察、比較することで検証した。仮説を「2年以上にわたりABCに頻回に参加しておりレートが1000以上で安定している中級(レート1000-1999)ユーザーは、対照期間と比較し、o1およびo3リリース後のコンテストにおいてレート変化量が減少している」と設定した。

Method

o1,o3による影響が顕著に観測されうる事象として、「長期間安定していた中級ユーザーのレートが突然下落する」を仮定し、以下の全てを満たすユーザーを"長期間レートが安定していた中級ユーザー"と定義し各個人のABCのrated出場結果を抽出した。

  • 2024年3月2日時点でレートが1000以上2000未満
  • コンテスト出場回数が合計100回以上(ARCを含む)
  • 2023年1月1日以降のABC終了時点でのレートが1000未満となったことが一度も無い

計938人が条件に合致した。比較する期間として、各期間にABCが計8回含まれるように以下のように設定した(執筆時期の関係からo3期のみ5回)。対照期および対照期以前はo3期,o1期との比較を主眼に、極力季節要因等の影響が少なくなるようo3期、o1期のそれぞれ1年前に設定した。対照期以前自体は、対照期のレート変化量が特異なものでないことを確認する目的も込めて設定している。

  • 対照期:2024-01-20 ~ 2024-03-09
  • 対照期以前:2023-09-16 ~ 2023-11-04
  • 4oリリース後(4o期):2024-05-18 ~ 2024-07-06
  • o1リリース後(o1期):2024-09-14 ~ 2024-11-02
  • 生成AI禁止令後(禁止期):2024-11-16 ~ 2025-01-04
  • o3リリース後(o3期):2025-02-01 ~ 2025-03-01

各期間の出場回数が一度でも3回に満たないユーザーは除外した上で各期間のABCによるレート変化量を抽出し、最終的に235人、データ総数9045件について統計解析を行った。

対照期とそれ以外の5期間それぞれを比較し、各ユーザーの該当期間のレート変化量の平均についてPaired t-testを用いて解析を行った。5回の多重比較となるためボンフェローニ補正を用いてp<0.01を有意水準に設定した。各個人の期間内レート変化量平均については分布の正規性をヒストグラム、Shapiro–Wilk testで確認した(各p=0.555, p=0.018, p=0.157, p=0.924, p=0.132, p=0.299)。対照期以前についてはp<0.05であり、Wilcoxon testも併用した。外れ値の割合についても四分位範囲を用いて調査したが、0.85-3.40%に留まっていた。また、ユーザーごとのレートの高低などユーザー固有のランダム効果がレート変化量に影響を及ぼしている可能性も考慮し、混合効果モデルを用いた解析も副次的に行った。

Result

ユーザーのレート変化量は対照期および対照期以前にて平均値、中央値ともに正の値となったのに対し、それ以降の期間については中央値はいずれも負の値、平均値はo1期、o3期において負の値となった(Figure 1, Figure 2)。

対応のあるt検定、混合効果モデルいずれにおいても、対照期と比較しo1期以降の3期間でレート変化量の有意な減少が認められ、とりわけo1期(t=4.953,p<0.001)、o3期(t=4.179,p<0.001)においてその減少量が大きかった。(Figure 2,Table 1,Table 2)

Figure 1: 各期間におけるレート変化量の分布

Figure_1.png
各期間内のすべてのレート変化量を箱ひげ図で表示

Figure 2: 各期間におけるレート変化量の平均、標準誤差と検定結果

Figure_2.png
各期間内のレート変化量の平均値を表示、エラーバーは標準誤差、**はp<0.001、*はp<0.01

Table 1: Paired t-test

control vs t-value p-value Mean Diff Diff SD 95% CI [Low, High]
before_control 0.100 0.921 0.101 15.549 [-1.897, 2.100]
4o 2.216 0.028 2.196 15.191 [ 0.243, 4.148]
o1 4.953 0.000 4.724 14.623 [ 2.845, 6.603]
o1(banned) 2.847 0.005 2.800 15.075 [ 0.862, 4.737]
o3 4.179 0.000 4.649 17.054 [ 2.457, 6.841]

※ 分布の正規性が確認できなかったbefore controlについてはWilcoxon Testも行い W = 13533.000, p-value = 0.750, Z = -0.318, r = 0.021 と、Paired t-test同様に有意差が無いことを確認した。

Table 2: 混合効果モデル

Coef. Std.Err. z-value p-value 95% CI
Intercept 3.884 0.710 5.468 0.000 [2.49, 5.28]
before control -0.057 1.008 -0.057 0.955 [-2.03, 1.92]
4o -2.321 1.005 -2.310 0.021 [-4.29, -0.35]
o1 -4.833 1.009 -4.790 0.000 [-6.81, -2.86]
o1(banned) -3.159 1.024 -3.087 0.002 [-5.17, -1.15]
o3 -4.744 1.144 -4.149 0.000 [-6.99, -2.50]

Discussion

今回私は、2年以上にわたりABCに頻回に参加しておりレートが1000以上で安定しているAtCoder中級ユーザーについて、対照期間と比較し、o1リリース以降のABCにおいてレート変化量が有意に減少していることを確認した。

レート変化量の低下幅は1回あたり平均5程度であり、ABCでのパフォーマンスに換算しておよそ50程度の低下があると推察される。この変化は対照期以前との比較では観察されず、また4o期との比較でも有意差を認めないため、o1およびo3の使用者が該当時期に一定数発生した事に起因すると考えることができる。さらにAtCoder社が生成AI利用の制限を発表した後には一時的に低下幅が縮小しており、制限には一定の効果があったと同時にAI利用者が一定数いたことを示唆する。o3期において再度低下幅が増大した背景には、o1と異なりo3が無料利用できることにより、もともと制限を無視ないし軽視していたが無課金のためo1が利用できなかった層がo3を利用してコンテストに参加するようになった可能性が考えられる。

長期レート安定者に対して平均50程度のパフォーマンスの低下がリリース以降のコンテスト数回にわたり発生するという結果は、順位及びレートの実力からの乖離という点で無視できないものであり、AtCoder社には積極的な対策が求められるとともに、中下級ユーザー側もレートに固執することを避け、新たなアルゴリズムの勉強など本質的かつ建設的な成長尺度に主眼を移す必要がある。

Limitation

  • ARCでのレート変化は観測していないためARCでレートが低下した後のABCではレートは上がりやすいのではないかなど、補足できていない因子が存在しうる。
  • 混合効果モデルによる解析はScale: 825.5499、Log-Likelihood: -43200.9417、Converged: No、ユーザー個人差によるランダム効果がCoef. 0.01と、モデルの妥当性は低い。
  • 各期間の計8回のコンテストにおいても、初期と後期で低下幅に差が出る可能性がある。
  • 中級古参ユーザーでありかつGPTを使用しているという場合もあり、その群を除外することができればより正確な分析が可能だが、それは困難。

研究手順詳細

追試可能となるよう手順詳細を付記しておきます。ただし3月8日にはレートが変化し完全に同一の研究対象者が補足できなくなる可能性が高いです。追試のためのAtCoderへのrequest増加を防ぐ目的も込めてcsvをgoogle drive共有としておきました(一応htmlも)。基本、無課金GPTに書かせて所々自分で手直ししているので、汚ぇですがお許しください。

[手順詳細]

① AtCoder社ランキングページよりrate1000-1999、出場回数100-9999、アクティブユーザーのみで検索し、以下のような形式で抽出(出来上がったものがこちらranking.html

ranking.html
<tbody>
    <tr>
        <td class="no-break"><span class="small gray">(1)</span> 1649</td>
        <td><a href="/ranking?contestType=algo&amp;f.Country=JP"><img src="//img.atcoder.jp/assets/flag/JP.png"></a> <img src="//img.atcoder.jp/assets/user/user-yellow-1.png" class="user-rating-stage-m"><a href="/users/yupiteru" class="username"><span class="user-yellow">yupiteru</span></a>
            <a href="/ranking?contestType=algo&amp;f.Affiliation=Recruit+Co.%2C+Ltd."><span class="ranking-affiliation break-all">Recruit Co., Ltd.</span></a></td>
        <td>1993</td>
        <td><b>2000</b></td>
        <td><b>2161</b></td>
        <td>256</td>
        <td>0</td>
    </tr>
    <tr>
        <td class="no-break"><span class="small gray">(2)</span> 1661</td>
        <td><a href="/ranking?contestType=algo&amp;f.Country=TW"><img src="//img.atcoder.jp/assets/flag/TW.png"></a> <img src="//img.atcoder.jp/assets/user/user-blue-4.png" class="user-rating-stage-m"><a href="/users/rensiyuan" class="username"><span class="user-blue">rensiyuan</span></a>
            <a href="/ranking?contestType=algo&amp;f.Affiliation=Chongqing+Bashu+madhouse"><span class="ranking-affiliation break-all">Chongqing Bashu madhouse</span></a></td>
        <td></td>
        <td><b>1997</b></td>
        <td><b>1997</b></td>
        <td>122</td>
        <td>0</td>
    </tr>
</tbody>

② 1で抽出したユーザーについて、個人のコンテスト成績表ページにrequestを送りresponseをスクレイピングして、2023/1/1以降のABCratedのみのデータを抽出し、一度もABC後レートが1000以下になっていないユーザーのデータをcsvとして保存(出来上がったものがこちらatcoder_user_fr1000_to1999_ratedABC.csv

makeCSV.py
import random
import re
import requests
import time
from bs4 import BeautifulSoup
import csv
from datetime import datetime

CSV_FILE = "atcoder_user_fr1000_to1999_ratedABC.csv"


def extract_usernames_from_file(file_path):
    """HTMLファイルからAtCoderのユーザー名を抽出"""
    with open(file_path, "r", encoding="utf-8") as file:
        html = file.read()  # ファイルからHTMLを読み込む
    
    soup = BeautifulSoup(html, "html.parser")
    users = []

    for row in soup.find_all("tr"):  # 各行を取得
        user_link = row.find("a", class_="username")  # ユーザー名が含まれる<a>タグを探す
        if user_link:
            username = user_link.text.strip()
            users.append(username)
    
    return users


def fetch_user_history(username):
    """AtCoderユーザーのレート履歴を取得"""
    url = f"https://atcoder.jp/users/{username}/history"
    response = requests.get(url)

    if response.status_code != 200:
        print(f"[Error] {username}: ページの取得に失敗しました。")
        return None
    
    soup = BeautifulSoup(response.text, "html.parser")
    table = soup.find("table")  # ヒストリーデータが含まれるテーブルを取得

    if not table:
        print(f"[Error] {username}: 履歴テーブルが見つかりません。")
        return None
    
    rows = table.find("tbody").find_all("tr")  # テーブルのボディ部から行を取得
    
    results = []
    for row in rows:
        cols = row.find_all("td")
        if len(cols) < 6:
            continue  # 不完全な行をスキップ

        date_str = cols[0].find("time").text.strip()  # '2025-03-01 22:40:00+0900' のような形式
        contest_name = cols[1].find("a").text.strip()  # コンテスト名
        performance = cols[3].text.strip()  # パフォーマンス
        new_rating = cols[4].text.strip()  # 新レート
        rating_diff = cols[5].text.strip()  # レート差分

        match = re.search(r"Beginner Contest (\d+)", contest_name)
        if not match:
            continue  # マッチしない場合はスキップ
        
        contest_name = f"Beginner Contest {match.group(1)}"

        # 日付をパース(タイムゾーン対応)
        try:
            contest_date = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S%z")
        except ValueError:
            continue  # 日付パースに失敗した場合はスキップ

        # 条件: 2023-01-01 以降のABCコンテストで、パフォーマンスが "-" でないものを対象にする
        if contest_date >= datetime(2023, 1, 1, tzinfo=contest_date.tzinfo) and "Beginner" in contest_name and performance != "-":
            try:
                performance = int(performance)
                new_rating = int(new_rating)
                rating_diff = int(rating_diff)
                results.append([username, date_str, contest_name, performance, new_rating, rating_diff])
            except ValueError:
                continue  # 数値変換に失敗した場合はスキップ

    return results

def save_to_csv(data):
    """データをCSVに追記"""
    with open(CSV_FILE, "a", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        for row in data:
            writer.writerow(row)

def filter_users(user_list):
    """レートが常に1000以上のユーザーを抽出し、CSVに記録"""
    valid_users = []

    # CSVファイルのヘッダーを設定(上書き保存)
    with open(CSV_FILE, "w", newline="", encoding="utf-8") as file:
        writer = csv.writer(file)
        writer.writerow(["Username", "Date", "Contest", "Performance", "NewRating", "RatingDiff"])  # ヘッダー

    for user in user_list:
        print(f"[INFO] ユーザー {user} のデータを取得中...")
        data = fetch_user_history(user)
        if not data : continue

        # すべての記録でレートが1000以上かチェック
        if all(new_rating >= 1000 for _, _, _, _, new_rating, _ in data):
            valid_users.append(data)  # valid_users に追加
            save_to_csv(data)  # CSV に記録
            print(f"[SUCCESS] {user} のデータを記録しました。")

        time.sleep(random.uniform(0.5, 1.5))  # API制限対策
    
    return valid_users  # valid_users を return する



# ユーザーを取得・フィルタリング
file_path = "ranking.html"
usernames = extract_usernames_from_file(file_path)

filtered_users = filter_users(usernames)

③ 対象期間を指定してデータを整理し統計解析

analysis.py
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats
import statsmodels.formula.api as smf
import statsmodels.api as sm
import numpy as np

out = open("result.txt",mode="w")

# 期間の定義
periods = {
    "control\n(2024/1~2)": ("2024-01-20", "2024-03-09"),
    "before_control\n(2023/9~10)": ("2023-09-16", "2023-11-04"),
    "4o": ("2024-05-18", "2024-07-06"),
    "o1": ("2024-09-14", "2024-11-02"),
    "o1 (banned)": ("2024-11-16", "2025-01-04"),
    "o3": ("2025-02-01", "2025-03-01"),
}

# **(1) CSVを読み込み**
df = pd.read_csv("atcoder_user_fr1000_to1999_ratedABC.csv")

# **(2) 期間ごとの分類**
def assign_period(date):
    date = pd.to_datetime(date).date()  # datetime.date に変換
    for period, (start, end) in periods.items():
        start_date = pd.to_datetime(start).date()
        end_date = pd.to_datetime(end).date()
        if start_date <= date <= end_date:
            return period
    return None

df["Date"] = pd.to_datetime(df["Date"]).dt.date
df["period"] = df["Date"].apply(assign_period)
df = df.dropna(subset=["period"])  # 期間外データ削除

# **(3) 参加回数が少なすぎるユーザーを除外(最低3回参加)**
user_counts = df.groupby("Username")["period"].value_counts().unstack(fill_value=0)
valid_users = user_counts[(user_counts >= 3).all(axis=1)].index
df = df[df["Username"].isin(valid_users)]
print("user number:",df["Username"].nunique(),file=out)

# **(4) 各期間の平均レート変化**
summary = df.groupby("period")["RatingDiff"].agg(["mean", "std", "count"]).reset_index()
print(summary,file=out)
print(stats.shapiro(df["RatingDiff"]),file=out)

# **(5) 対応のあるt検定**
def paired_t_test(df, period1, period2):
    def detect_outliers_iqr(data):
        Q1 = np.percentile(data, 25)
        Q3 = np.percentile(data, 75)
        IQR = Q3 - Q1
        lower_bound = Q1 - 1.5 * IQR
        upper_bound = Q3 + 1.5 * IQR
        outliers = [x for x in data if x < lower_bound or x > upper_bound]
        
        outlier_ratio = len(outliers) / len(data)
        return len(outliers), outlier_ratio
    def wilcoxon_test(x1, x2):
        # Wilcoxonの符号付き順位検定
        w, p = stats.wilcoxon(x1, x2)
        
        # Zスコアの計算
        z = stats.norm.ppf(p / 2) if p < 1 else 0  # 片側検定のp値からZスコアを求める
        
        # 効果量(r)の計算
        n = len(x1)
        r = abs(z) / np.sqrt(n) if n > 0 else np.nan  # n = 0 だとエラーになるため

        # 結果を出力
        print(f"W = {w:.3f}, p-value = {p:.3f}, Z = {z:.3f}, r = {r:.3f}",file=out)
        return w, p, z, r
    """ 同じユーザーの2つの期間のRatingDiffの対応のあるt検定を実施 """
    df_pivot = df.groupby(["Username", "period"])["RatingDiff"].mean().unstack()  # 各ユーザーの期間ごとの平均を取得
    df_pivot = df_pivot.dropna(subset=[period1, period2])  # 両方の期間にデータがあるユーザーのみ

    x1 = df_pivot[period1]
    x2 = df_pivot[period2]
    if period2=="before_control\n(2023/9~10)" : 
        num_outliers, outlier_ratio = detect_outliers_iqr(x1)
        print(period1,f"number of outliers: {num_outliers}, outlier ratio: {outlier_ratio:.2%}",file=out)
        print(period1,stats.shapiro(x1),file=out)
        plt.figure(figsize=(10, 5))
        sns.histplot(x1, bins=20, kde=True)
        plt.title("Histogram of "+ period1)
        plt.xlabel("mean amount of rate change")
        plt.show()
        wilcoxon_test(x1,x2)
    num_outliers, outlier_ratio = detect_outliers_iqr(x2)
    print(period2,f"number of outliers: {num_outliers}, outlier ratio: {outlier_ratio:.2%}",file=out)
    print(period2,stats.shapiro(x2),file=out)
    plt.figure(figsize=(10, 5))
    sns.histplot(x2, bins=20, kde=True)
    plt.title("Histogram of "+ period2)
    plt.xlabel("mean amount of rate change")
    plt.show()
    diff = x1 - x2  # 差分

    # t検定
    t_stat, p_value = stats.ttest_rel(x1, x2)

    # 各期間の統計量
    mean1, std1 = x1.mean(), x1.std()
    mean2, std2 = x2.mean(), x2.std()
    
    # 差分の統計量
    mean_diff = diff.mean()
    std_diff = diff.std()
    
    # 95%信頼区間
    ci_low, ci_high = stats.t.interval(0.95, len(diff)-1, loc=mean_diff, scale=stats.sem(diff))

    return t_stat, p_value, mean1, std1, mean2, std2, mean_diff, std_diff, (ci_low, ci_high)


t_results = []
for period in ["before_control\n(2023/9~10)", "4o", "o1", "o1 (banned)", "o3"]:
    t_stat, p_value, mean1, std1, mean2, std2, mean_diff, std_diff, ci = paired_t_test(df, "control\n(2024/1~2)", period)
    t_results.append([period, t_stat, p_value, mean1, std1, mean2, std2, mean_diff, std_diff, ci[0], ci[1]])

# DataFrame にまとめて表示
columns = ["Period", "t-value", "p-value", "Control Mean", "Control SD", "Period Mean", "Period SD", 
           "Mean Diff", "Diff SD", "95% CI Low", "95% CI High"]
df_results = pd.DataFrame(t_results, columns=columns)
print(df_results,file=out)

# **(6) 混合効果モデル**
df["period"] = pd.Categorical(df["period"], categories=periods.keys(), ordered=True)
model = smf.mixedlm("RatingDiff ~ period", df, groups=df["Username"])
results = model.fit(reml=True, maxiter=1000)
print(results.summary(),file=out)
residuals = model.fit().resid
sm.qqplot(residuals, line='s')
plt.show()


# **(7) 可視化**
plt.figure(figsize=(12, 6))
sns.boxplot(x="period", y="RatingDiff", data=df, order=periods.keys())
plt.axhline(0, color="red", linestyle="dashed")
plt.title("Distribution of rate changes over periods")
plt.xticks(rotation=30)
plt.show()

plt.figure(figsize=(10, 5))
sns.histplot(df["RatingDiff"], bins=20, kde=True)
plt.title("Histogram of rate changes")
plt.xlabel("amount of rate change")
plt.show()

plt.figure(figsize=(12, 6))
sns.lineplot(x="Date", y="RatingDiff", hue="Username", data=df, legend=False, alpha=0.5)
plt.axhline(0, color="red", linestyle="dashed")
plt.title("Transition of rate changes over time series")
plt.show()

def plot_RatingDiff_with_significance(df, control_period="control\n(2024/1~2)"):
    """
    各期間のRatingDiffの平均値と標準誤差をプロットし、有意差があるものにアスタリスクを付与する
    """
    # 各期間ごとにRatingDiffの平均と標準誤差を計算
    period_stats = df.groupby("period")["RatingDiff"].agg(["mean", "sem"]).reset_index()
    
    # 並び順をperiodsの定義に合わせる
    periods = ["before_control\n(2023/9~10)", "control\n(2024/1~2)", "4o", "o1", "o1 (banned)", "o3"]
    period_stats = period_stats.set_index("period").reindex(periods).reset_index()
    
    # 有意差検定(controlと比較)
    p_values = []
    for period in periods:
        if period == control_period:
            p_values.append(None)
            continue
        
        df_control = df[df["period"] == control_period]["RatingDiff"].dropna()
        df_target = df[df["period"] == period]["RatingDiff"].dropna()
        
        if len(df_control) > 0 and len(df_target) > 0:
            t_stat, p_value = stats.ttest_ind(df_control, df_target, equal_var=False)
            p_values.append(p_value)
        else:
            p_values.append(None)

    # ⭐ 有意差のある期間にアスタリスクを付与
    significance_labels = []
    for p in p_values:
        if p is None:
            significance_labels.append("")
        elif p < 0.001:
            significance_labels.append("**")  # p < 0.001
        elif p < 0.01:
            significance_labels.append("*")   # p < 0.01
        else:
            significance_labels.append("")

    # グラフの描画
    fig, ax = plt.subplots(figsize=(10, 6))
    x_positions = np.arange(len(periods))
    y_means = period_stats["mean"]
    y_errors = period_stats["sem"]

    ax.bar(x_positions, y_means, yerr=y_errors, capsize=5, color=["gray" if p == control_period else "blue" for p in periods], alpha=0.7)
    
    # アスタリスクの描画
    for i, label in enumerate(significance_labels):
        if label:
            ax.text(x_positions[i], y_means[i] + y_errors[i] + 2, label, ha='center', fontsize=14, color="red")

    # 軸ラベル・タイトル
    ax.set_xticks(x_positions)
    ax.set_xticklabels(periods, rotation=45)
    ax.set_ylabel("Mean rate change")
    ax.set_title("Comparison of Rate Change Across Periods for Ratings 1000–1999")

    plt.tight_layout()
    plt.show()

plot_RatingDiff_with_significance(df)

一応result詳細もリンク貼っておきます。result.txt

さいごに

ぼくのこの感覚、めちゃくちゃ正確だったみたいです。

テーマの非有用性と校正も査読もされてない荒削りな原稿である点を除けば仮説、検証方針、実際のメソッド、結果、どれもPhDとして最低限恥ずかしくない、ザ・サイエンスな研究だと自画自賛しているので、GPTに英訳させてAI関連のハゲタカジャーナルとかに投稿したら業績の足しにならないかな。ハゲタカジャーナルとかに

R7.3.9 21:30 追記

大変光栄なことにchokudai御代にお読みいただき、X上でコメントをいただきました。
長期安定ならばインフレを反映しそもそも年50-100程度下がるのではないかという意見はごもっともで、私としても再考してみると「長期安定ユーザー」の定義が甘いのではないかという点が一番クリティカルな問題点であると考えています。
対応策は

  • 「長期安定」を2020年頃から1000以上というより厳しい定義とする(コンテスト成績表ページにアクセスしたにも関わらずなぜより広く拾わなかったのか…おそらく被験者数が減りすぎることを恐れたのでしょう)
  • 本データであれば、4o以前を通して変化量を観測し横ばいであることが確認できるユーザーのみを対象とする(検定方法は何が適切でしょう)

などが考えられます。貴重なレビューをいただいたわけなので本来ならばリバイズすべきですが、本論文はそもそものコンセプトから悪ふざけが過ぎるので

リジェクトです!

本研究の目的は知的好奇心以外にあるとすれば落ち込んでいる古参ユーザーを励ますことなので達成された方が何人かでもいればよしとしましょう

あれほど多重比較しないと言ったのに、2023年1月と2024年1月比較のfigureも追加しておきます。2023/1から1000以上で2025/3/2時点で1000-1999のユーザーという条件もあってか、1年での変化は無しです(見た目的には増加)。2023/1とAI期の検定はo1期のみ有意差ありでした

Figure_2.png

自分の平時の研究もこれくらい楽しめれば論文量産できるのにな…

R7.3.10 00:30 追々記

学者の端くれの端くれの端くれとして、すぐに検証可能であるにも関わらずリジェクトの体で逃げるのはやはり誠実さに欠くと考え、後者の方針で検証してみました。
検証可能な範囲で可能性として潰すべきは、該当の235ユーザーが4o以前において

  • レート変化量に単調減少のトレンドが見られる
  • 前半に増加し後半に減少する

の2項だと考えました。
前者は追加figureから可能性は低そうですが、Mann-Kendall検定なるものを利用してみました(使ったことないので間違ってたらすみません)
p-valueが0.05未満かつslopeが負の値の被験者を割り出すと235人中5人(2.13%)でした。基準はありませんが、少なくとも今回長期安定と定義したユーザーにおいては4o期以前に年5程度(パフォーマンスにして50)の低下は認められる可能性が低いと言えそうです。
(3.11 21:30追記)mann-kendallのコードが間違っていたのと、そもそも安定を確認すべきはこの場合レート変化量でなくレートそのものなので、修正して解析したところp-valueが0.05未満かつslopeが-0.137未満(年50以上のレート低下トレンド)ユーザーは32人で13.62%でした。2.13%よりは多いですが、やはり全体で単調減少のトレンドが見られるとは言い難いためこの修正結果はこの先の議論に影響しないと判断しました。

突貫検証コード
mannkendall.py
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats
import statsmodels.formula.api as smf
import statsmodels.api as sm
import numpy as np
from scipy.stats import linregress
from pymannkendall import original_test as mk_test

out = open("result2.txt",mode="w")

# 期間の定義
periods = {
    "control\n(2024/1~2)": ("2024-01-20", "2024-03-09"),
    "before_control\n(2023/9~10)": ("2023-09-16", "2023-11-04"),
    "4o": ("2024-05-18", "2024-07-06"),
    "o1": ("2024-09-14", "2024-11-02"),
    "o1 (banned)": ("2024-11-16", "2025-01-04"),
    "o3": ("2025-02-01", "2025-03-01"),
}

# **(1) CSVを読み込み**
df = pd.read_csv("atcoder_user_fr1000_to1999_ratedABC.csv")

# **(2) 期間ごとの分類**
def assign_period(date):
    date = pd.to_datetime(date).date()  # datetime.date に変換
    for period, (start, end) in periods.items():
        start_date = pd.to_datetime(start).date()
        end_date = pd.to_datetime(end).date()
        if start_date <= date <= end_date:
            return period
    return None

df["Date"] = pd.to_datetime(df["Date"]).dt.date
df["period"] = df["Date"].apply(assign_period)

# **(3) 参加回数が少なすぎるユーザーを除外(最低3回参加)**
user_counts = df.groupby("Username")["period"].value_counts().unstack(fill_value=0)
valid_users = user_counts[(user_counts >= 3).all(axis=1)].index
df = df[df["Username"].isin(valid_users)]

# 指定期間(2023年1月1日〜2024年4月30日)のデータに絞り込み
df['Date'] = pd.to_datetime(df['Date'])
df = df[(df['Date'] >= '2023-01-01') & (df['Date'] <= '2024-04-30')]

# ユーザーごとにソート
df = df.sort_values(['Username', 'Date'])

# ユーザーごとの結果を格納するリスト
results = []

# 各ユーザーごとにMann-Kendall検定を適用
for username, group in df.groupby('Username'):
    
    # Mann-Kendall検定の実行
    mk_result = mk_test(group['NewRating'], alpha=0.05)
    
    # 結果をリストに格納
    results.append({
        'Username': username,
        'n': len(group),
        'p-value': mk_result.p,
        'Trend': 'No Trend' if mk_result.p >= 0.05 else 'Declining',
        'Slope': mk_result.slope
    })

# 結果をデータフレーム化
results_df = pd.DataFrame(results)

# CSVファイルとして出力
results_df.to_csv('rating_trend_results_filtered.csv', index=False)

print("指定期間の検定結果を 'rating_trend_results_filtered.csv' に出力しました。")



# 安定ユーザーの定義
stable_users = results_df[(results_df['p-value'] >= 0.05) & (abs(results_df['Slope']) < 0.137)]

# 低下傾向ユーザーの定義
declining_users = results_df[(results_df['p-value'] < 0.05) & (results_df['Slope'] < -0.137)]

# 上昇傾向ユーザーの定義
improving_users = results_df[(results_df['p-value'] < 0.05) & (results_df['Slope'] > 0.137)]

# 各グループの人数
total_users = len(results_df)
stable_count = len(stable_users)
declining_count = len(declining_users)
improving_count = len(improving_users)

# 各割合を計算
stable_ratio = stable_count / total_users
declining_ratio = declining_count / total_users
improving_ratio = improving_count / total_users

# 結果を出力
print(f'総ユーザー数: {total_users}')
print(f'安定ユーザー数: {stable_count} ({stable_ratio:.2%})')
print(f'低下傾向ユーザー数: {declining_count} ({declining_ratio:.2%})')
print(f'上昇傾向ユーザー数: {improving_count} ({improving_ratio:.2%})')

後者の可能性については前期(2023.1-2023.12)と後期(2024.1-2024.4)でPaired t-testを行いました。結果は有意差無し(t-value=1.401,p-value=0.163,95%CI -0.357~2.115)なので、後者のトレンドも可能性として低いと考えます。なお、無いことの証明は厳密には困難なので、参考程度に、期間の切れ目を1月ごとに変えて検定しまくってみました。切れ目を7月から12月まで移動したところ、7月31日以前と以降(t-value=3.870,p-value<0.001,willcoxonでもp=0.001)、8月31日以前と以降(t-value=1.993,p-value=0.047)、9月30日以前と以降(t-value=2.197,p-value=0.029,willcoxonでもp=0.020)は低下傾向を認めました。多重比較となるため差として意義があるのは7月31日以前と以降の比較のみと考えますが、トレンドとして2023年前半にはレート上昇幅が大きくその後下がっていったと捉えることもある程度尤もらしいです。
Figure_3.png

以上から、少なくとも今回の被験者235名については長期安定者に観測されうる年5程度の低下(パフォーマンスにして50程度の低下)は、4o以前には明確には観測されないものの、期間内で上昇傾向をとったのち低下傾向を辿った可能性は否定できず本論文のresultはそれを反映していたに過ぎないと読むこともできます。

やはり御代の指摘通り「初めてから時間が経てば経つほどレート変化が減るのは当たり前と言えば当たり前」という現象を観測していただけの可能性を否定しきれず、よって本研究はrejectされました。

R7.3.10 11:30 追々々記 3.11 21:30修正

追記の検証方針と追々記の検証方針にズレが生じていたので、2023/1-2024/4でMann-Kendall検定がp-value>=0.05かつ-0.0137<slope<0.0137の、より厳密な定義での長期安定ユーザー24人12人についてPaired t-testしてみました。流石に被験者減りすぎて有意差出ませんでした笑。完
Figure_3.png

control vs t-value p-value Diff SD 95% CI [Low, High]
before_control 0.093 0.927 15.580 [-9.480, 10.319]
4o -0.511 0.620 13.021 [-10.193, 6.353]
o1 -0.013 0.990 16.893 [-10.796, 10.670]
o1(banned) -1.947 0.077 13.491 [-16.156, 0.988]
o3 2.919 0.014 13.695 [ 2.837, 20.241]

はしがき

いつも全部盛りぶち込み重回帰とか安易な混合効果モデルとかばっかりで論文書いてるから、こういう由緒正しいPaired t-testとかを丁寧に適用する部分は非常に勉強になった。無課金GPTすごいわ

あと、年50-100のインフレって過去問精進で前々から感じてはいたけど、実は広く共有された業界のコンセンサスだったのか。手法に何らかの形で組み込めればよかったな。

21
3
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
21
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?