Kaggleの自動化コード、集積地 (自由に改良して、このスクラップに書き込んだり、ブログ等で公開してほしいです)

コンペが始まったら通知する
このbotのコードです。

コンペの終了日までの日数を可視化するコード
このコードをCloudFunctionsで毎日動かしています。
import io
import matplotlib.pyplot as plt
import seaborn as sns
import logging
from flask import Response, Request
from dotenv import load_dotenv
from datetime import datetime as dt
from pytz import timezone
import os
import base64
import requests
import json
load_dotenv()
is_debug_mode = os.getenv("DEBUG", "True") == "True"
if is_debug_mode:
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
else:
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
def plot_competition_fig():
os.environ['KAGGLE_USERNAME'] = os.getenv('KAGGLE_USER_NAME')
os.environ['KAGGLE_KEY'] = os.getenv('KAGGLE_KEY')
from kaggle.api.kaggle_api_extended import KaggleApi
kaggle_api = KaggleApi()
kaggle_api.authenticate()
now = dt.now(timezone('UTC'))
competitions = []
competitions_list = kaggle_api.competitions_list()
for competition in competitions_list:
if getattr(competition, 'awardsPoints') and not getattr(competition, 'submissionsDisabled'):
deadline = getattr(competition, 'deadline').astimezone(timezone('UTC'))
enabled_date = getattr(competition, 'enabledDate').astimezone(timezone('UTC'))
diff = deadline - now
if diff.days >= 0:
competitions.append({
'title': getattr(competition, 'title'),
'enabledDate': enabled_date,
'deadline': deadline,
'days_to_end': diff.days
})
# 残り日数が長い順にソート
competitions.sort(key=lambda x: x['days_to_end'], reverse=True)
plt.figure(figsize=(12, 8)) # 縦幅を調整
sns.set(style='white')
plt.rcParams["font.size"] = 18
shift_amount = 0.1 # 左にシフトする量
vertical_offset = -0.1 # 全体を少し下にシフトする
for i, competition in enumerate(competitions):
start_date = competition['enabledDate']
days_to_end = competition['days_to_end']
days_from_start = (now - start_date).days
start_pos = 0.5 - (min(days_from_start, 90) / 180)
end_pos = 0.5 + (min(days_to_end, 100) / 180)
start_pos -= shift_amount
end_pos -= shift_amount
plt.plot((start_pos, 0.5 - shift_amount), (i + vertical_offset, i + vertical_offset), linewidth=6.0, c='blue')
plt.plot((0.5 - shift_amount, end_pos), (i + vertical_offset, i + vertical_offset), linewidth=6.0, c='orange')
if days_to_end > 100:
plt.text(end_pos - 0.005, i + vertical_offset + 0.02, "~", fontsize=20, rotation=90, ha='center', va='center')
plt.text(end_pos - 0.008, i + vertical_offset + 0.02, "~", fontsize=20, rotation=90, ha='center', va='center')
plt.text(1.03, i + vertical_offset, f'{days_to_end}', va='center', fontsize=20, ha='right', c='orange')
plt.text(1.08, i + vertical_offset, 'days', va='center', fontsize=8, ha='right')
plt.text(-0.06, i + 0.1 + vertical_offset, competition['title'], ha='left', va='bottom', fontsize=20)
plt.yticks([])
plt.xticks([])
plt.title('Kaggle Competitions: Countdown to Deadline', fontsize=22)
plt.text(0.4, -0.01, dt.strftime(now, '%Y-%m-%d'), fontsize='14', ha='center', va='top', transform=plt.gca().transAxes)
plt.plot([0.5 - shift_amount, 0.5 - shift_amount], [-0.5, -0.4], color='black', linestyle='--', linewidth=1.5)
plt.ylim(-0.5, len(competitions) - 0.5)
plt.xlim(-0.1, 1.1)
plt.subplots_adjust(left=0.03, right=0.97, bottom=0.08, top=0.92, hspace=0.3)
# 画像をメモリに保存し、base64エンコード
buf = io.BytesIO()
plt.savefig(buf, format='png')
buf.seek(0)
image_base64 = base64.b64encode(buf.read()).decode('utf-8')
buf.close()
return image_base64
def post_kaggle_deadlines_info(request: Request):
try:
image_base64 = plot_competition_fig()
twitter_auth_keys = {
"consumer_key": os.getenv("CONSUMER_KEY"),
"consumer_secret": os.getenv("CONSUMER_SECRET"),
"access_token": os.getenv("ACCESS_TOKEN"),
"access_token_secret": os.getenv("ACCESS_TOKEN_SECRET"),
"bearer_token": os.getenv("BEARER_TOKEN")
}
request_data = {
"platform": ["twitter"],
"message": "Deadlines for Ongoing Kaggle Competitions! 🕒",
"media": image_base64,
"mdeia": None,
"twitter_auth_keys": twitter_auth_keys,
"is_debug": False
}
url = os.getenv("POST_URL")
# POSTリクエストを送信
response = requests.post(url, json=request_data)
response.raise_for_status()
response_body = json.dumps({"message": response.text})
return Response(response_body, content_type="application/json"), 200
except Exception as e:
logging.error(f"Error occurred: {e}")
response_body = json.dumps({"error": str(e)})
return Response(response_body, content_type="application/json"), 500
if __name__ == "__main__":
post_kaggle_deadlines_info(None)

Notebookが公開されていたら、Discordに通知する。
コードはGistで公開しています。
(注)3月13日のLTではクラウドで動かしていると話しましたが、常時稼働のMac miniで動かしていました。

DiscussionにTopicが作られていたら、Discordに通知する
1. Discussionの投稿が自分の登録メールに届くようにする!
コンペのDiscussionで、Follow forumを選択しておくことで、トピックが作成されるたびにメールが届きます。Discordに通知したいコンペで、Follow forum選択します。
※ Follow forum and commentsを選択すると、コメントが投稿されてもメールが届く
※ GASを利用するため、メールはgmailにする必要があります。Kaggleの登録メールがgmailではない場合はgmailへ自動転送などすれば、同じ感じで使えると思います。
2. メールにラベルを自動でつけるようにする。
トピックを作成したときのメールの件名には、「[コンペ名] Topic: 」が先頭に含まれます。この件名の場合には、自動でラベルが付くように設定します。
gmailで検索ワードを入れて、検索オプションをクリックします。
フィルタを作成をクリックします。
「ラベルをつけるを」選択し、「ラベル名を入力」。
「一致するスレッドにもフィルタを適用する」を選択し、フィルタを作成をクリックします。
これでこれまで届いたメールと、これから届くメールに指定のラベルがつくようになりました。
3. GASの設定
https://script.google.com/home/ にアクセスし、新しいプロジェクトを作成します。
エディタが表示されるので、次のコードを貼り付けます。
function discord(postMsg) {
const webhooks = ''; // discordのwebhookURLを入力
const payload = {
'content': postMsg,
'username': 'Kerneler',
'parse': 'full'
};
const params = {
'method': 'post',
'payload': payload,
'muteHttpExceptions': true
};
response = UrlFetchApp.fetch(webhooks, params);
}
function mails() {
const searchQuery = "label:llmprompt"; // gmailのlabel名を入力。「llmprompt」というラベルになっているので、自分のラベルに修正してください。
const checkSpanMinute = 60;
const dt = new Date();
dt.setMinutes(dt.getMinutes() - checkSpanMinute);
const threads = GmailApp.search(searchQuery);
const msgs = GmailApp.getMessagesForThreads(threads);
for (const thread of threads) {
const lastMsgDt = thread.getLastMessageDate();
if (lastMsgDt.getTime() < dt.getTime()) {
break;
}
const messages = thread.getMessages();
for (const message of messages) {
const msgPlainText = message.getPlainBody();
// 改行を適切に整形する
const formattedText = msgPlainText
.replace(/(\r\n|\r|\n){3,}/g, '\n\n') // 3回以上の連続した改行を2回の改行に置換
.replace(/\\\*{2,}/g, '**')
.replace(/\_{2,}/g, '__')
.replace(/\s*(\r\n|\r|\n)\s*/g, '\n') // 改行の前後の空白を削除し、単一の改行に置換
.replace(/\s{2,}/g, ' ') // 2つ以上の連続した空白を1つの空白に置換
.trim(); // 前後の空白を削除
// 2つ目のNewTopic以降に絞る
function extractTextAfterSecondNewTopic(formattedText) {
const regex = /^.*?New Topic.*?New Topic(.*)/s;
const match = formattedText.match(regex);
if (match && match[1]) {
return match[1].trim();
} else {
return "";
}
}
const extractedText = extractTextAfterSecondNewTopic(formattedText);
// 末尾の "Reply on Kaggle" 以降のテキストを削除
const cleanedText = extractedText.replace(/Reply on Kaggle.*$/s, '').trim();
// 1行目をH1形式にして "タイトル:" をつける
const lines = cleanedText.split("\\n");
let title = lines[0].trim();
title = title.replace(/^in \*\*LLM Prompt Recovery\*\*: /, '').replace(/\*\*/g, '');
const titleWithPrefix = `# New Discussion Topic: ${title}`;
lines[0] = titleWithPrefix;
const formattText = lines.join("\\n");
// 投稿用のテキストを作成
const postMsg = `${formattText}`;
discord(postMsg);
}
}
}
左にある「トリガー」、「トリガーを追加」と進む。
実行する関数を選択に「mails」、イベントのソースを選択に「時間主導型」、時間の感覚を選択に「1時間」を選択し保存を選択し、設定完了です。

サブミット時間を計測するコード
2022年7月version
tqdmによる表示に対応したversion
スコアリング後の経過時間だけではなく、スコアリング中に、何分経ったか表示したい場合にはこちらのコードになります。
次の1の設定を行い、2のコードを実行します。
- アプリケーションを設定する
tqdmのプログレスバーの通知のDiscordの箇所にアプリケーションの設定方法があるので、これを行います。
ここで取得したものが、次のコードの「DISCORD_BOT_TOKEN 」と「DISCORD_CHANNEL_ID」になります。
import sys
from kaggle.api.kaggle_api_extended import KaggleApi
import datetime
import time
import requests
import os
from dotenv import load_dotenv
from tqdm.contrib.discord import tqdm
load_dotenv()
WEBHOOK_URL = os.getenv('WEBHOOK_URL')
if not WEBHOOK_URL:
raise ValueError("WEBHOOK_URLが設定されていません。 .envファイルを確認してください。")
DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
if not DISCORD_BOT_TOKEN:
raise ValueError("DISCORD_BOT_TOKENが設定されていません。 .envファイルを確認してください。")
DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
if not DISCORD_CHANNEL_ID:
raise ValueError("DISCORD_CHANNEL_IDが設定されていません。 .envファイルを確認してください。")
def send_discord_notification(message):
webhook_url = WEBHOOK_URL
payload = {'content': message}
headers = {'Content-Type': 'application/json'}
response = requests.post(webhook_url, json=payload, headers=headers)
print(message)
return response
def main():
send_discord_notification('submit check start')
api = KaggleApi()
api.authenticate()
# コマンドライン引数がある場合はその番号、なければ 0 番目の submission を対象とする
submission_index = int(sys.argv[1]) if len(sys.argv) > 1 else 0
submissions = api.competition_submissions(COMPETITION)
# submissionが存在するかチェック
if not submissions:
print("No submissions found.")
sys.exit()
if submission_index >= len(submissions):
print("Invalid submission index.")
sys.exit()
latest_submission = submissions[submission_index]
latest_ref = str(latest_submission.ref)
submit_time = latest_submission.date
if latest_submission.status != 'pending':
print('サブミットされているnotebookはなし')
sys.exit()
pbar = tqdm(total=540, token=DISCORD_BOT_TOKEN, channel_id=DISCORD_CHANNEL_ID)
while True:
# 最新の submission 一覧を取得して、対象の submission を探す
submissions = api.competition_submissions(COMPETITION)
current_submission = next((sub for sub in submissions if str(sub.ref) == latest_ref), None)
if current_submission is None:
print("Submission not found.")
pbar.close()
sys.exit(1)
status = current_submission.status
now = datetime.datetime.utcnow() # submit_timeがUTCであることを前提
elapsed_time = int((now - submit_time).total_seconds() / 60) + 1
if status == 'complete':
message = (
f'run-time: {elapsed_time} min, LB: {current_submission.publicScore}, '
f'file_name: {current_submission.fileName}, description: {current_submission.description}\n'
f'{current_submission.url}'
)
print('\r' + message)
send_discord_notification(message)
sys.exit(0)
else:
print('\r' + f'elapsed time: {elapsed_time} min, file_name: {current_submission.fileName}', end='')
pbar.update(1) # 1分ごとに更新
time.sleep(60)
if __name__ == '__main__':
COMPETITION = 'titanic' # コンペ名を設定
main()