diff --git a/.assets-revision b/.assets-revision index 5c22104e..700aa319 100644 --- a/.assets-revision +++ b/.assets-revision @@ -5,4 +5,8 @@ # is a git revision (branch name like `main`, a tag, or a specific commit # sha). Override at runtime with the ASSETS_REVISION env var. repo: ChilleD/WebHarbor -revision: 54882a6a66a17a3e43455057e7c9e0d103cd8b81 +# Pinned to HF PR #37 (adds kaggle.tar.gz on top of a main that already includes +# the current 16 sites) so this reviewer PR builds end-to-end before the assets PR +# is merged. Maintainer: once https://huggingface.co/datasets/ChilleD/WebHarbor/discussions/37 +# is rebased onto HF main and merged, bump this to the merged commit SHA. +revision: refs/pr/37 diff --git a/Dockerfile b/Dockerfile index 1e86b1d0..ad6d5d9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -33,6 +33,6 @@ COPY control_server.py /opt/control_server.py COPY site_runner.py /opt/site_runner.py RUN chmod +x /opt/websyn_start.sh -EXPOSE 8101 40000-40015 +EXPOSE 8101 40000-40016 CMD ["/opt/websyn_start.sh"] diff --git a/control_server.py b/control_server.py index 4b6b995e..c2e22cbc 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', 'merriam_webster', + 'coursera', 'espn', 'merriam_webster', 'kaggle', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/sites/kaggle/_health.py b/sites/kaggle/_health.py new file mode 100644 index 00000000..e8a5f701 --- /dev/null +++ b/sites/kaggle/_health.py @@ -0,0 +1,45 @@ +"""kaggle mirror health check.""" +from healthcheck import random_user + + +def run(p): + p.assert_get('home', '/', must_contain='Kaggle') + p.assert_get('competitions', '/competitions', must_contain='Competitions') + p.assert_get('search fraud', '/search?q=fraud', must_contain='fraud') + p.assert_get( + 'competition detail', + '/competitions/credit-default-risk-2026', + must_contain='ROC AUC', + ) + p.assert_get('dataset detail', '/datasets/credit-card-fraud-transactions', + must_contain='About this Dataset') + p.assert_get('rankings', '/rankings', must_contain='Rankings') + + user = random_user() + html = p.assert_get('register page', '/register') + token = p.csrf(html) + if not token: + p.check('register csrf', False, 'no csrf') + return + p.assert_post('register submit', '/register', { + 'csrf_token': token, + 'username': user['username'], + 'email': user['email'], + 'password': user['password'], + 'confirm': user['password'], + }, accept_status=(200, 302, 303)) + + p.get('/logout') + + html = p.assert_get('login page', '/login') + token = p.csrf(html) + if not token: + p.check('login csrf', False, 'no csrf') + return + p.assert_post('login submit', '/login', { + 'csrf_token': token, + 'email': user['email'], + 'password': user['password'], + }, accept_status=(200, 302, 303)) + + p.assert_get('account page', '/account', accept_status=(200, 302)) diff --git a/sites/kaggle/app.py b/sites/kaggle/app.py new file mode 100644 index 00000000..bb44fa49 --- /dev/null +++ b/sites/kaggle/app.py @@ -0,0 +1,1397 @@ +""" +Kaggle Mirror — Full-stack Flask application. + +A deterministic offline mirror of kaggle.com for web-agent benchmarks. + +Entity model (data-science competition platform): + User — competitor with progression tiers + performance medals + Competition — hosted ML challenge (prize, metric, deadline, leaderboard) + Submission — a leaderboard entry for a competition + CompetitionEntry — a user joining a competition (the "register"/transaction flow) + Dataset — community dataset with usability score, votes, downloads + Notebook — public code (kernel), optionally linked to comp/dataset + Model — pretrained model with framework + variations + Course — Kaggle Learn micro-course + Discussion — forum thread; Comment — replies + Vote / Bookmark — polymorphic interactions across entity types + Follow — social graph (user -> user) +""" +import json +import os +import re +from datetime import datetime, date +from pathlib import Path + +# ---------------------------------------------------------------------------- +# Pinned mirror clock. The image is built once and evaluated at any future +# point; freeze "today" so date-relative tasks ("active competitions", +# "deadline this month") behave deterministically. Chosen to sit just after +# the newest seeded timestamp so recent uploads stay "recent" and every +# historical anchor stays firmly in the past. +# ---------------------------------------------------------------------------- +MIRROR_REFERENCE_DATE = datetime(2026, 6, 22, 12, 0, 0) + + +def mirror_now() -> datetime: + return MIRROR_REFERENCE_DATE + + +def mirror_today() -> date: + return MIRROR_REFERENCE_DATE.date() + + +from flask import (Flask, render_template, request, redirect, url_for, flash, + jsonify, abort) +from flask_sqlalchemy import SQLAlchemy +from flask_login import (LoginManager, UserMixin, login_user, logout_user, + login_required, current_user) +from flask_bcrypt import Bcrypt +from flask_wtf import FlaskForm, CSRFProtect +from wtforms import (StringField, PasswordField, TextAreaField, SelectField, + HiddenField) +from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional as OptionalV + +from seed_data import ( + PERFORMANCE_TIERS, COMPETITION_CATEGORIES, DISCUSSION_FORUMS, ML_FRAMEWORKS, + LICENSES, PROGRAMMING_LANGUAGES, BENCHMARK_USERS, NOTABLE_USERS, COMPETITIONS, + LEADERBOARDS, DATASETS, NOTEBOOKS, MODELS, COURSES, DISCUSSIONS, + DISCUSSION_COMMENTS, +) + +# ---------------------------------------------------------------------------- +# Flask setup +# ---------------------------------------------------------------------------- +ROOT = Path(__file__).parent +app = Flask(__name__) +app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "kaggle-mirror-dev-secret-key-change-me") +app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{ROOT / 'instance' / 'kaggle.db'}" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +(ROOT / "instance").mkdir(exist_ok=True) + +db = SQLAlchemy(app) +bcrypt = Bcrypt(app) +csrf = CSRFProtect(app) +login_manager = LoginManager(app) +login_manager.login_view = "login" +login_manager.login_message_category = "info" + +TIER_COLORS = { + "Novice": "#5ac995", "Contributor": "#00aaff", "Expert": "#95319b", + "Master": "#f96517", "Grandmaster": "#dca917", +} +MEDAL_EMOJI = {"gold": "🥇", "silver": "🥈", "bronze": "🥉"} + + +@app.template_filter("fromjson") +def _fromjson_filter(value): + try: + if isinstance(value, (list, dict)): + return value + return json.loads(value) + except Exception: + return [] + + +@app.template_filter("commafy") +def _commafy(value): + try: + return f"{int(value):,}" + except Exception: + return value + + +@app.template_filter("reltime") +def _reltime(value): + """Human 'time ago' relative to the mirror clock.""" + if not value: + return "" + if isinstance(value, str): + try: + value = datetime.fromisoformat(value) + except Exception: + return value + if isinstance(value, date) and not isinstance(value, datetime): + value = datetime(value.year, value.month, value.day) + delta = mirror_now() - value + days = delta.days + if days < 0: + return "just now" + if days == 0: + return "today" + if days == 1: + return "yesterday" + if days < 30: + return f"{days} days ago" + if days < 365: + return f"{days // 30} months ago" + return f"{days // 365} years ago" + + +# ------------------------------------------------------------ +# Database models +# ------------------------------------------------------------ +class User(db.Model, UserMixin): + __tablename__ = "users" + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(180), unique=True, nullable=False, index=True) + username = db.Column(db.String(80), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(200), nullable=False) + display_name = db.Column(db.String(120), default="") + bio = db.Column(db.Text, default="") + avatar_url = db.Column(db.String(300), default="/static/images/avatars/default.png") + location = db.Column(db.String(120), default="") + occupation = db.Column(db.String(120), default="") + organization = db.Column(db.String(120), default="") + website = db.Column(db.String(200), default="") + tier = db.Column(db.String(20), default="Novice") + tiers_json = db.Column(db.Text, default="{}") # per-category tiers + points = db.Column(db.Integer, default=0) + gold = db.Column(db.Integer, default=0) + silver = db.Column(db.Integer, default=0) + bronze = db.Column(db.Integer, default=0) + comp_rank = db.Column(db.Integer, nullable=True) # global competitions rank + is_org = db.Column(db.Boolean, default=False) + is_admin = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=mirror_now) + + @property + def tier_color(self): + return TIER_COLORS.get(self.tier, "#888") + + @property + def category_tiers(self): + try: + return json.loads(self.tiers_json or "{}") + except Exception: + return {} + + @property + def total_medals(self): + return (self.gold or 0) + (self.silver or 0) + (self.bronze or 0) + + +class Competition(db.Model): + __tablename__ = "competitions" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + subtitle = db.Column(db.String(300), default="") + category = db.Column(db.String(40), index=True) + host = db.Column(db.String(160), default="Kaggle") + owner_username = db.Column(db.String(80), default="Kaggle") + reward = db.Column(db.String(60), default="Knowledge") + reward_value = db.Column(db.Integer, default=0) # USD, 0 for non-cash + metric = db.Column(db.String(80), default="") + num_teams = db.Column(db.Integer, default=0) + deadline = db.Column(db.Date, nullable=True) + tags_json = db.Column(db.Text, default="[]") + thumbnail = db.Column(db.String(120), default="") + description = db.Column(db.Text, default="") + + submissions = db.relationship("Submission", backref="competition", + cascade="all, delete-orphan", lazy="dynamic") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + @property + def is_active(self): + return self.deadline is None or self.deadline >= mirror_today() + + @property + def days_left(self): + if not self.deadline: + return None + return (self.deadline - mirror_today()).days + + @property + def reward_display(self): + return self.reward + + +class Submission(db.Model): + __tablename__ = "submissions" + id = db.Column(db.Integer, primary_key=True) + competition_id = db.Column(db.Integer, db.ForeignKey("competitions.id"), index=True) + rank = db.Column(db.Integer) + team_name = db.Column(db.String(160)) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) + score = db.Column(db.Float) + submitted_at = db.Column(db.Date) + + +class CompetitionEntry(db.Model): + """A user joining a competition — the register/transaction flow.""" + __tablename__ = "competition_entries" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + competition_id = db.Column(db.Integer, db.ForeignKey("competitions.id"), index=True) + team_name = db.Column(db.String(160), default="") + accepted_rules = db.Column(db.Boolean, default=True) + joined_at = db.Column(db.DateTime, default=mirror_now) + competition = db.relationship("Competition") + + +class Dataset(db.Model): + __tablename__ = "datasets" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + subtitle = db.Column(db.String(300), default="") + owner_username = db.Column(db.String(80), default="") + size = db.Column(db.String(40), default="") + size_bytes = db.Column(db.BigInteger, default=0) + file_count = db.Column(db.Integer, default=1) + file_types = db.Column(db.String(120), default="CSV") + usability = db.Column(db.Float, default=0.0) + upvotes = db.Column(db.Integer, default=0) + downloads = db.Column(db.Integer, default=0) + views = db.Column(db.Integer, default=0) + license = db.Column(db.String(120), default="") + tags_json = db.Column(db.Text, default="[]") + last_updated = db.Column(db.Date, nullable=True) + thumbnail = db.Column(db.String(120), default="") + description = db.Column(db.Text, default="") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + +class Notebook(db.Model): + __tablename__ = "notebooks" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + author_username = db.Column(db.String(80), default="") + language = db.Column(db.String(20), default="Python") + votes = db.Column(db.Integer, default=0) + comments = db.Column(db.Integer, default=0) + medal = db.Column(db.String(10), nullable=True) + best_score = db.Column(db.String(40), nullable=True) + runtime = db.Column(db.String(40), default="") + last_run = db.Column(db.Date, nullable=True) + linked_competition = db.Column(db.String(120), nullable=True) + linked_dataset = db.Column(db.String(120), nullable=True) + thumbnail = db.Column(db.String(120), default="") + tags_json = db.Column(db.Text, default="[]") + description = db.Column(db.Text, default="") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + @property + def medal_emoji(self): + return MEDAL_EMOJI.get(self.medal, "") + + +class Model(db.Model): + __tablename__ = "models" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(200), nullable=False) + owner_username = db.Column(db.String(80), default="") + framework = db.Column(db.String(40), default="") + variations = db.Column(db.Integer, default=1) + downloads = db.Column(db.Integer, default=0) + upvotes = db.Column(db.Integer, default=0) + license = db.Column(db.String(120), default="") + tags_json = db.Column(db.Text, default="[]") + last_updated = db.Column(db.Date, nullable=True) + thumbnail = db.Column(db.String(120), default="") + description = db.Column(db.Text, default="") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + +class Course(db.Model): + __tablename__ = "courses" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False, index=True) + title = db.Column(db.String(160), nullable=False) + lessons = db.Column(db.Integer, default=0) + hours = db.Column(db.Integer, default=0) + level = db.Column(db.String(20), default="Beginner") + icon = db.Column(db.String(10), default="📘") + tags_json = db.Column(db.Text, default="[]") + description = db.Column(db.Text, default="") + lesson_titles_json = db.Column(db.Text, default="[]") + + @property + def tags(self): + try: + return json.loads(self.tags_json or "[]") + except Exception: + return [] + + @property + def lesson_titles(self): + try: + return json.loads(self.lesson_titles_json or "[]") + except Exception: + return [] + + +class Discussion(db.Model): + __tablename__ = "discussions" + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(140), unique=True, nullable=False, index=True) + title = db.Column(db.String(240), nullable=False) + author_username = db.Column(db.String(80), default="") + forum = db.Column(db.String(60), index=True) + votes = db.Column(db.Integer, default=0) + comment_count = db.Column(db.Integer, default=0) + pinned = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=mirror_now) + body = db.Column(db.Text, default="") + + comments = db.relationship("Comment", backref="discussion", + cascade="all, delete-orphan", lazy="dynamic") + + +class Comment(db.Model): + __tablename__ = "comments" + id = db.Column(db.Integer, primary_key=True) + discussion_id = db.Column(db.Integer, db.ForeignKey("discussions.id"), index=True) + author_username = db.Column(db.String(80), default="") + body = db.Column(db.Text, default="") + votes = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=mirror_now) + + +class Vote(db.Model): + """Polymorphic upvote: entity_type in {dataset, notebook, model, discussion, competition}.""" + __tablename__ = "votes" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + entity_type = db.Column(db.String(20), index=True) + entity_id = db.Column(db.Integer, index=True) + created_at = db.Column(db.DateTime, default=mirror_now) + + +class Bookmark(db.Model): + __tablename__ = "bookmarks" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + entity_type = db.Column(db.String(20), index=True) + entity_id = db.Column(db.Integer, index=True) + created_at = db.Column(db.DateTime, default=mirror_now) + + +class Follow(db.Model): + __tablename__ = "follows" + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), index=True) + target_username = db.Column(db.String(80), index=True) + created_at = db.Column(db.DateTime, default=mirror_now) + + +@login_manager.user_loader +def load_user(user_id): + return db.session.get(User, int(user_id)) + + +# ------------------------------------------------------------ +# Forms +# ------------------------------------------------------------ +class LoginForm(FlaskForm): + email = StringField("Email", validators=[DataRequired(), Email()]) + password = PasswordField("Password", validators=[DataRequired()]) + + +class RegisterForm(FlaskForm): + email = StringField("Email", validators=[DataRequired(), Email()]) + username = StringField("Username", validators=[DataRequired(), Length(min=3, max=40)]) + password = PasswordField("Password", validators=[DataRequired(), Length(min=6)]) + confirm = PasswordField("Confirm password", validators=[DataRequired(), EqualTo("password")]) + + +class ProfileForm(FlaskForm): + display_name = StringField("Display name", validators=[OptionalV(), Length(max=120)]) + bio = TextAreaField("Bio", validators=[OptionalV(), Length(max=600)]) + location = StringField("Location", validators=[OptionalV(), Length(max=120)]) + occupation = StringField("Occupation", validators=[OptionalV(), Length(max=120)]) + organization = StringField("Organization", validators=[OptionalV(), Length(max=120)]) + website = StringField("Website", validators=[OptionalV(), Length(max=200)]) + + +class PasswordForm(FlaskForm): + current = PasswordField("Current password", validators=[DataRequired()]) + new = PasswordField("New password", validators=[DataRequired(), Length(min=6)]) + confirm = PasswordField("Confirm new password", validators=[DataRequired(), EqualTo("new")]) + + +class JoinCompetitionForm(FlaskForm): + team_name = StringField("Team name", validators=[DataRequired(), Length(max=120)]) + accept_rules = HiddenField() + + +class DiscussionForm(FlaskForm): + title = StringField("Title", validators=[DataRequired(), Length(max=240)]) + forum = SelectField("Forum", choices=[(f, f) for f in DISCUSSION_FORUMS], validators=[DataRequired()]) + body = TextAreaField("Body", validators=[DataRequired(), Length(min=10)]) + + +class CommentForm(FlaskForm): + body = TextAreaField("Comment", validators=[DataRequired(), Length(min=1)]) + + +# ------------------------------------------------------------ +# Search scoring (token-overlap, NOT strict AND) +# ------------------------------------------------------------ +STOPWORDS = {"the", "a", "an", "of", "for", "to", "and", "or", "in", "on", "with", + "by", "is", "are", "how", "what", "best", "top", "find", "show", "me"} + + +def _tokenize(q: str): + return [t for t in re.findall(r"[a-z0-9.+#-]+", (q or "").lower()) + if len(t) >= 2 and t not in STOPWORDS] + + +def _score(hay: str, tokens): + if not tokens: + return 1 + hay = hay.lower() + return sum(1 for t in tokens if t in hay) + + +def _comp_hay(c): + return " ".join([c.slug, c.title, c.subtitle or "", c.category or "", + c.host or "", c.metric or "", c.description or "", + " ".join(c.tags)]) + + +def _dataset_hay(d): + return " ".join([d.slug, d.title, d.subtitle or "", d.owner_username or "", + d.license or "", d.description or "", d.file_types or "", + " ".join(d.tags)]) + + +def _notebook_hay(n): + return " ".join([n.slug, n.title, n.author_username or "", n.language or "", + n.description or "", n.linked_competition or "", + n.linked_dataset or "", " ".join(n.tags)]) + + +def _model_hay(m): + return " ".join([m.slug, m.title, m.owner_username or "", m.framework or "", + m.license or "", m.description or "", " ".join(m.tags)]) + + +def _user_hay(u): + return " ".join([u.username, u.display_name or "", u.bio or "", + u.location or "", u.occupation or "", u.organization or "", + u.tier or ""]) + + +def _discussion_hay(d): + return " ".join([d.slug, d.title, d.author_username or "", d.forum or "", + d.body or ""]) + + +# ------------------------------------------------------------ +# Context helpers +# ------------------------------------------------------------ +@app.context_processor +def inject_globals(): + return dict( + current_year=mirror_now().year, + nav_forums=DISCUSSION_FORUMS, + comp_categories=COMPETITION_CATEGORIES, + TIER_COLORS=TIER_COLORS, + MEDAL_EMOJI=MEDAL_EMOJI, + ) + + +def user_by_name(username): + return User.query.filter_by(username=username).first() + + +def has_voted(entity_type, entity_id): + if not current_user.is_authenticated: + return False + return Vote.query.filter_by(user_id=current_user.id, entity_type=entity_type, + entity_id=entity_id).first() is not None + + +def has_bookmarked(entity_type, entity_id): + if not current_user.is_authenticated: + return False + return Bookmark.query.filter_by(user_id=current_user.id, entity_type=entity_type, + entity_id=entity_id).first() is not None + + +def is_following(username): + if not current_user.is_authenticated: + return False + return Follow.query.filter_by(user_id=current_user.id, target_username=username).first() is not None + + +app.jinja_env.globals.update( + has_voted=has_voted, has_bookmarked=has_bookmarked, + is_following=is_following, user_by_name=user_by_name, +) + + +# ------------------------------------------------------------ +# Homepage + static pages +# ------------------------------------------------------------ +@app.route("/") +def index(): + featured = Competition.query.filter(Competition.category.in_(["Featured", "Research"])) \ + .order_by(Competition.reward_value.desc()).limit(4).all() + active_comps = Competition.query.order_by(Competition.num_teams.desc()).limit(6).all() + hot_datasets = Dataset.query.order_by(Dataset.upvotes.desc()).limit(6).all() + trending_notebooks = Notebook.query.order_by(Notebook.votes.desc()).limit(4).all() + courses = Course.query.limit(4).all() + return render_template("index.html", featured=featured, active_comps=active_comps, + hot_datasets=hot_datasets, trending_notebooks=trending_notebooks, + courses=courses) + + +@app.route("/_health") +def health(): + return {"ok": True, "site": "kaggle", + "competitions": Competition.query.count(), + "datasets": Dataset.query.count()} + + +# ------------------------------------------------------------ +# Competitions +# ------------------------------------------------------------ +@app.route("/competitions") +def competitions_list(): + q = request.args.get("q", "").strip() + category = request.args.get("category", "").strip() + status = request.args.get("status", "").strip() # active | completed + sort = request.args.get("sort", "relevance" if q else "teams") + + query = Competition.query + if category: + query = query.filter(Competition.category == category) + comps = query.all() + + if status == "active": + comps = [c for c in comps if c.is_active] + elif status == "completed": + comps = [c for c in comps if not c.is_active] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, c) for c in comps if (s := _score(_comp_hay(c), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].num_teams)) + comps = [c for _, c in scored] + else: + if sort == "prize": + comps.sort(key=lambda c: -c.reward_value) + elif sort == "deadline": + comps.sort(key=lambda c: (c.deadline or date.max)) + elif sort == "newest": + comps.sort(key=lambda c: -c.id) + else: # teams + comps.sort(key=lambda c: -c.num_teams) + + return render_template("competitions.html", comps=comps, q=q, + category=category, status=status, sort=sort) + + +@app.route("/competitions/") +def competition_detail(slug): + c = Competition.query.filter_by(slug=slug).first_or_404() + leaderboard = c.submissions.order_by(Submission.rank.asc()).all() + host_user = user_by_name(c.owner_username) + notebooks = Notebook.query.filter_by(linked_competition=slug).order_by(Notebook.votes.desc()).all() + entry = None + joined = False + if current_user.is_authenticated: + entry = CompetitionEntry.query.filter_by(user_id=current_user.id, competition_id=c.id).first() + joined = entry is not None + join_form = JoinCompetitionForm() + return render_template("competition_detail.html", c=c, leaderboard=leaderboard, + host_user=host_user, notebooks=notebooks, joined=joined, + entry=entry, join_form=join_form, tab=request.args.get("tab", "overview")) + + +@app.route("/competitions//leaderboard") +def competition_leaderboard(slug): + c = Competition.query.filter_by(slug=slug).first_or_404() + leaderboard = c.submissions.order_by(Submission.rank.asc()).all() + return render_template("leaderboard.html", c=c, leaderboard=leaderboard) + + +@app.route("/competitions//join", methods=["POST"]) +@login_required +def competition_join(slug): + c = Competition.query.filter_by(slug=slug).first_or_404() + if not c.is_active: + flash("This competition has closed — you can no longer join.", "error") + return redirect(url_for("competition_detail", slug=slug)) + form = JoinCompetitionForm() + if form.validate_on_submit(): + existing = CompetitionEntry.query.filter_by(user_id=current_user.id, competition_id=c.id).first() + if existing: + flash("You have already joined this competition.", "info") + else: + entry = CompetitionEntry(user_id=current_user.id, competition_id=c.id, + team_name=form.team_name.data.strip(), accepted_rules=True) + db.session.add(entry) + c.num_teams = (c.num_teams or 0) + 1 + db.session.commit() + flash(f"You're in! Team '{entry.team_name}' joined {c.title}.", "success") + else: + flash("Please enter a team name and accept the rules.", "error") + return redirect(url_for("competition_detail", slug=slug)) + + +# ------------------------------------------------------------ +# Datasets +# ------------------------------------------------------------ +@app.route("/datasets") +def datasets_list(): + q = request.args.get("q", "").strip() + tag = request.args.get("tag", "").strip() + file_type = request.args.get("file_type", "").strip() + sort = request.args.get("sort", "relevance" if q else "hottest") + + datasets = Dataset.query.all() + if tag: + datasets = [d for d in datasets if tag in d.tags] + if file_type: + datasets = [d for d in datasets if file_type.lower() in (d.file_types or "").lower()] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, d) for d in datasets if (s := _score(_dataset_hay(d), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].upvotes)) + datasets = [d for _, d in scored] + else: + if sort == "votes": + datasets.sort(key=lambda d: -d.upvotes) + elif sort == "downloads": + datasets.sort(key=lambda d: -d.downloads) + elif sort == "usability": + datasets.sort(key=lambda d: -d.usability) + elif sort == "updated": + datasets.sort(key=lambda d: (d.last_updated or date.min), reverse=True) + elif sort == "size": + datasets.sort(key=lambda d: -d.size_bytes) + else: # hottest + datasets.sort(key=lambda d: -(d.upvotes + d.downloads // 20)) + + all_tags = sorted({t for d in Dataset.query.all() for t in d.tags}) + return render_template("datasets.html", datasets=datasets, q=q, tag=tag, + file_type=file_type, sort=sort, all_tags=all_tags) + + +@app.route("/datasets/") +def dataset_detail(slug): + d = Dataset.query.filter_by(slug=slug).first_or_404() + owner = user_by_name(d.owner_username) + notebooks = Notebook.query.filter_by(linked_dataset=slug).order_by(Notebook.votes.desc()).all() + return render_template("dataset_detail.html", d=d, owner=owner, notebooks=notebooks) + + +@app.route("/datasets//download") +def dataset_download(slug): + d = Dataset.query.filter_by(slug=slug).first_or_404() + d.downloads = (d.downloads or 0) + 1 + db.session.commit() + flash(f"Download started: {d.title} ({d.size}).", "success") + return redirect(url_for("dataset_detail", slug=slug)) + + +# ------------------------------------------------------------ +# Notebooks (Code) +# ------------------------------------------------------------ +@app.route("/code") +def notebooks_list(): + q = request.args.get("q", "").strip() + language = request.args.get("language", "").strip() + medal = request.args.get("medal", "").strip() + sort = request.args.get("sort", "relevance" if q else "votes") + + notebooks = Notebook.query.all() + if language: + notebooks = [n for n in notebooks if n.language == language] + if medal: + notebooks = [n for n in notebooks if n.medal == medal] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, n) for n in notebooks if (s := _score(_notebook_hay(n), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].votes)) + notebooks = [n for _, n in scored] + else: + if sort == "comments": + notebooks.sort(key=lambda n: -n.comments) + elif sort == "recent": + notebooks.sort(key=lambda n: (n.last_run or date.min), reverse=True) + else: + notebooks.sort(key=lambda n: -n.votes) + + return render_template("notebooks.html", notebooks=notebooks, q=q, + language=language, medal=medal, sort=sort) + + +@app.route("/code/") +def notebook_detail(slug): + n = Notebook.query.filter_by(slug=slug).first_or_404() + author = user_by_name(n.author_username) + comp = Competition.query.filter_by(slug=n.linked_competition).first() if n.linked_competition else None + dataset = Dataset.query.filter_by(slug=n.linked_dataset).first() if n.linked_dataset else None + return render_template("notebook_detail.html", n=n, author=author, comp=comp, dataset=dataset) + + +# ------------------------------------------------------------ +# Models +# ------------------------------------------------------------ +@app.route("/models") +def models_list(): + q = request.args.get("q", "").strip() + framework = request.args.get("framework", "").strip() + sort = request.args.get("sort", "relevance" if q else "downloads") + + models = Model.query.all() + if framework: + models = [m for m in models if m.framework == framework] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, m) for m in models if (s := _score(_model_hay(m), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].downloads)) + models = [m for _, m in scored] + else: + if sort == "votes": + models.sort(key=lambda m: -m.upvotes) + elif sort == "updated": + models.sort(key=lambda m: (m.last_updated or date.min), reverse=True) + else: + models.sort(key=lambda m: -m.downloads) + + return render_template("models.html", models=models, q=q, + framework=framework, sort=sort, frameworks=ML_FRAMEWORKS) + + +@app.route("/models/") +def model_detail(slug): + m = Model.query.filter_by(slug=slug).first_or_404() + owner = user_by_name(m.owner_username) + return render_template("model_detail.html", m=m, owner=owner) + + +# ------------------------------------------------------------ +# Learn (courses) +# ------------------------------------------------------------ +@app.route("/learn") +def learn(): + courses = Course.query.all() + return render_template("learn.html", courses=courses) + + +@app.route("/learn/") +def course_detail(slug): + course = Course.query.filter_by(slug=slug).first_or_404() + return render_template("course_detail.html", course=course) + + +# ------------------------------------------------------------ +# Discussions +# ------------------------------------------------------------ +@app.route("/discussions") +def discussions_list(): + q = request.args.get("q", "").strip() + forum = request.args.get("forum", "").strip() + sort = request.args.get("sort", "relevance" if q else "hot") + + discussions = Discussion.query.all() + if forum: + discussions = [d for d in discussions if d.forum == forum] + + tokens = _tokenize(q) + if tokens: + min_req = max(1, len(tokens) // 2) + scored = [(s, d) for d in discussions if (s := _score(_discussion_hay(d), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], -sc[1].votes)) + discussions = [d for _, d in scored] + else: + if sort == "recent": + discussions.sort(key=lambda d: d.created_at or mirror_now(), reverse=True) + elif sort == "comments": + discussions.sort(key=lambda d: -d.comment_count) + else: # hot — pinned first, then votes + discussions.sort(key=lambda d: (not d.pinned, -d.votes)) + + return render_template("discussions.html", discussions=discussions, q=q, + forum=forum, sort=sort) + + +@app.route("/discussions/") +def discussion_detail(slug): + d = Discussion.query.filter_by(slug=slug).first_or_404() + author = user_by_name(d.author_username) + comments = d.comments.order_by(Comment.votes.desc()).all() + comment_form = CommentForm() + return render_template("discussion_detail.html", d=d, author=author, + comments=comments, comment_form=comment_form) + + +@app.route("/discussions/new", methods=["GET", "POST"]) +@login_required +def discussion_new(): + form = DiscussionForm() + if form.validate_on_submit(): + base = re.sub(r"[^a-z0-9]+", "-", form.title.data.lower()).strip("-")[:120] or "thread" + slug = base + n = 2 + while Discussion.query.filter_by(slug=slug).first(): + slug = f"{base}-{n}" + n += 1 + d = Discussion(slug=slug, title=form.title.data.strip(), forum=form.forum.data, + author_username=current_user.username, body=form.body.data.strip(), + votes=0, comment_count=0, pinned=False, created_at=mirror_now()) + db.session.add(d) + db.session.commit() + flash("Discussion posted.", "success") + return redirect(url_for("discussion_detail", slug=slug)) + return render_template("discussion_new.html", form=form) + + +@app.route("/discussions//comment", methods=["POST"]) +@login_required +def discussion_comment(slug): + d = Discussion.query.filter_by(slug=slug).first_or_404() + form = CommentForm() + if form.validate_on_submit(): + c = Comment(discussion_id=d.id, author_username=current_user.username, + body=form.body.data.strip(), votes=0, created_at=mirror_now()) + db.session.add(c) + d.comment_count = (d.comment_count or 0) + 1 + db.session.commit() + flash("Comment posted.", "success") + else: + flash("Comment cannot be empty.", "error") + return redirect(url_for("discussion_detail", slug=slug)) + + +# ------------------------------------------------------------ +# Rankings + user profiles +# ------------------------------------------------------------ +@app.route("/rankings") +def rankings(): + category = request.args.get("category", "competitions") + users = User.query.filter_by(is_org=False).all() + tier_weight = {t: i for i, t in enumerate(PERFORMANCE_TIERS)} + + def keyf(u): + ct = u.category_tiers.get(category, u.tier) + return (-tier_weight.get(ct, 0), -(u.points or 0)) + + users.sort(key=keyf) + return render_template("rankings.html", users=users, category=category, + categories=["competitions", "datasets", "notebooks", "discussions"]) + + +@app.route("/user/") +def user_profile(username): + u = User.query.filter_by(username=username).first_or_404() + datasets = Dataset.query.filter_by(owner_username=username).order_by(Dataset.upvotes.desc()).all() + notebooks = Notebook.query.filter_by(author_username=username).order_by(Notebook.votes.desc()).all() + models = Model.query.filter_by(owner_username=username).order_by(Model.downloads.desc()).all() + discussions = Discussion.query.filter_by(author_username=username).order_by(Discussion.votes.desc()).all() + hosted = Competition.query.filter_by(owner_username=username).all() + follower_count = Follow.query.filter_by(target_username=username).count() + return render_template("user_profile.html", u=u, datasets=datasets, notebooks=notebooks, + models=models, discussions=discussions, hosted=hosted, + follower_count=follower_count) + + +# ------------------------------------------------------------ +# Global search +# ------------------------------------------------------------ +@app.route("/search") +def search(): + q = request.args.get("q", "").strip() + scope = request.args.get("type", "all") + tokens = _tokenize(q) + min_req = max(1, len(tokens) // 2) if tokens else 0 + + def run(items, hayf, sortkey): + if not tokens: + return [] + scored = [(s, it) for it in items if (s := _score(hayf(it), tokens)) >= min_req] + scored.sort(key=lambda sc: (-sc[0], sortkey(sc[1]))) + return [it for _, it in scored] + + comps = run(Competition.query.all(), _comp_hay, lambda c: -c.num_teams) if scope in ("all", "competitions") else [] + datasets = run(Dataset.query.all(), _dataset_hay, lambda d: -d.upvotes) if scope in ("all", "datasets") else [] + notebooks = run(Notebook.query.all(), _notebook_hay, lambda n: -n.votes) if scope in ("all", "code") else [] + models = run(Model.query.all(), _model_hay, lambda m: -m.downloads) if scope in ("all", "models") else [] + users = run(User.query.all(), _user_hay, lambda u: -(u.points or 0)) if scope in ("all", "users") else [] + discussions = run(Discussion.query.all(), _discussion_hay, lambda d: -d.votes) if scope in ("all", "discussions") else [] + + total = len(comps) + len(datasets) + len(notebooks) + len(models) + len(users) + len(discussions) + return render_template("search.html", q=q, scope=scope, total=total, + comps=comps, datasets=datasets, notebooks=notebooks, + models=models, users=users, discussions=discussions) + + +# ------------------------------------------------------------ +# Auth +# ------------------------------------------------------------ +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("index")) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(email=form.email.data.lower().strip()).first() + if user and bcrypt.check_password_hash(user.password_hash, form.password.data): + login_user(user) + flash("Welcome back!", "success") + return redirect(request.args.get("next") or url_for("index")) + flash("Invalid email or password.", "error") + return render_template("login.html", form=form) + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if current_user.is_authenticated: + return redirect(url_for("index")) + form = RegisterForm() + if form.validate_on_submit(): + email = form.email.data.lower().strip() + username = form.username.data.strip() + if User.query.filter_by(email=email).first(): + flash("That email is already registered.", "error") + elif User.query.filter_by(username=username).first(): + flash("That username is taken.", "error") + else: + user = User( + email=email, username=username, + password_hash=bcrypt.generate_password_hash(form.password.data).decode(), + display_name=username, tier="Novice", + tiers_json=json.dumps({"competitions": "Novice", "datasets": "Novice", + "notebooks": "Novice", "discussions": "Novice"}), + avatar_url="/static/images/avatars/default.png", + created_at=mirror_now(), + ) + db.session.add(user) + db.session.commit() + login_user(user) + flash("Welcome to Kaggle!", "success") + return redirect(url_for("index")) + return render_template("register.html", form=form) + + +@app.route("/logout") +def logout(): + logout_user() + flash("Signed out.", "info") + return redirect(url_for("index")) + + +@app.route("/account") +@login_required +def account(): + entries = CompetitionEntry.query.filter_by(user_id=current_user.id).order_by(CompetitionEntry.joined_at.desc()).all() + bookmarks = Bookmark.query.filter_by(user_id=current_user.id).all() + votes = Vote.query.filter_by(user_id=current_user.id).all() + follows = Follow.query.filter_by(user_id=current_user.id).all() + bm = [(_resolve_entity(b.entity_type, b.entity_id), b) for b in bookmarks] + bm = [(e, b) for e, b in bm if e is not None] + return render_template("account.html", entries=entries, bookmarks=bm, + votes=votes, follows=follows) + + +@app.route("/account/edit", methods=["GET", "POST"]) +@login_required +def account_edit(): + form = ProfileForm(obj=current_user) + if form.validate_on_submit(): + current_user.display_name = form.display_name.data + current_user.bio = form.bio.data + current_user.location = form.location.data + current_user.occupation = form.occupation.data + current_user.organization = form.organization.data + current_user.website = form.website.data + db.session.commit() + flash("Profile updated.", "success") + return redirect(url_for("account")) + return render_template("account_edit.html", form=form) + + +@app.route("/account/password", methods=["GET", "POST"]) +@login_required +def change_password(): + form = PasswordForm() + if form.validate_on_submit(): + if not bcrypt.check_password_hash(current_user.password_hash, form.current.data): + flash("Current password incorrect.", "error") + else: + current_user.password_hash = bcrypt.generate_password_hash(form.new.data).decode() + db.session.commit() + flash("Password updated.", "success") + return redirect(url_for("account")) + return render_template("change_password.html", form=form) + + +@app.route("/account/delete", methods=["POST"]) +@login_required +def account_delete(): + user = db.session.get(User, current_user.id) + db.session.delete(user) + db.session.commit() + logout_user() + flash("Account deleted.", "info") + return redirect(url_for("index")) + + +# ------------------------------------------------------------ +# Interactions: vote / bookmark / follow (JSON + form fallback) +# ------------------------------------------------------------ +_ENTITY_MODELS = { + "competition": Competition, "dataset": Dataset, "notebook": Notebook, + "model": Model, "discussion": Discussion, +} +_VOTE_FIELD = {"competition": None, "dataset": "upvotes", "notebook": "votes", + "model": "upvotes", "discussion": "votes"} + + +def _resolve_entity(etype, eid): + model = _ENTITY_MODELS.get(etype) + if not model: + return None + return db.session.get(model, eid) + + +@app.route("/api/vote", methods=["POST"]) +@login_required +def api_vote(): + etype = request.form.get("entity_type") or (request.json or {}).get("entity_type") + eid = request.form.get("entity_id") or (request.json or {}).get("entity_id") + try: + eid = int(eid) + except (TypeError, ValueError): + return jsonify(error="bad entity_id"), 400 + if etype not in _ENTITY_MODELS: + return jsonify(error="bad entity_type"), 400 + ent = _resolve_entity(etype, eid) + if ent is None: + return jsonify(error="not found"), 404 + existing = Vote.query.filter_by(user_id=current_user.id, entity_type=etype, entity_id=eid).first() + field = _VOTE_FIELD.get(etype) + if existing: + db.session.delete(existing) + if field: + setattr(ent, field, max(0, (getattr(ent, field) or 0) - 1)) + voted = False + else: + db.session.add(Vote(user_id=current_user.id, entity_type=etype, entity_id=eid)) + if field: + setattr(ent, field, (getattr(ent, field) or 0) + 1) + voted = True + db.session.commit() + count = getattr(ent, field) if field else None + if request.is_json or request.headers.get("Accept", "").startswith("application/json"): + return jsonify(ok=True, voted=voted, count=count) + flash("Vote recorded." if voted else "Vote removed.", "success") + return redirect(request.referrer or url_for("index")) + + +@app.route("/api/bookmark", methods=["POST"]) +@login_required +def api_bookmark(): + etype = request.form.get("entity_type") or (request.json or {}).get("entity_type") + eid = request.form.get("entity_id") or (request.json or {}).get("entity_id") + try: + eid = int(eid) + except (TypeError, ValueError): + return jsonify(error="bad entity_id"), 400 + if etype not in _ENTITY_MODELS: + return jsonify(error="bad entity_type"), 400 + if _resolve_entity(etype, eid) is None: + return jsonify(error="not found"), 404 + existing = Bookmark.query.filter_by(user_id=current_user.id, entity_type=etype, entity_id=eid).first() + if existing: + db.session.delete(existing) + bookmarked = False + else: + db.session.add(Bookmark(user_id=current_user.id, entity_type=etype, entity_id=eid)) + bookmarked = True + db.session.commit() + if request.is_json or request.headers.get("Accept", "").startswith("application/json"): + return jsonify(ok=True, bookmarked=bookmarked) + flash("Saved." if bookmarked else "Removed from saved.", "success") + return redirect(request.referrer or url_for("index")) + + +@app.route("/api/follow", methods=["POST"]) +@login_required +def api_follow(): + username = request.form.get("username") or (request.json or {}).get("username") + target = User.query.filter_by(username=username).first() + if target is None: + return jsonify(error="not found"), 404 + if target.id == current_user.id: + return jsonify(error="cannot follow yourself"), 400 + existing = Follow.query.filter_by(user_id=current_user.id, target_username=username).first() + if existing: + db.session.delete(existing) + following = False + else: + db.session.add(Follow(user_id=current_user.id, target_username=username)) + following = True + db.session.commit() + if request.is_json or request.headers.get("Accept", "").startswith("application/json"): + return jsonify(ok=True, following=following) + flash("Followed." if following else "Unfollowed.", "success") + return redirect(request.referrer or url_for("user_profile", username=username)) + + +# ------------------------------------------------------------ +# Error handlers +# ------------------------------------------------------------ +@app.errorhandler(404) +def not_found(e): + return render_template("404.html"), 404 + + +@app.errorhandler(500) +def server_error(e): + return render_template("500.html"), 500 + + +# ------------------------------------------------------------ +# Seeding (idempotent — gate every function as a whole) +# ------------------------------------------------------------ +def _d(iso): + return date.fromisoformat(iso) if iso else None + + +def _dt(iso): + return datetime.fromisoformat(iso) if iso else mirror_now() + + +def seed_benchmark_users(): + if User.query.filter_by(email="alice.j@test.com").first(): + return + for u in BENCHMARK_USERS: + db.session.add(User( + email=u["email"], username=u["username"], + password_hash=bcrypt.generate_password_hash(u["password"]).decode(), + display_name=u["display_name"], bio=u["bio"], tier=u["tier"], + tiers_json=json.dumps(u["tiers"]), points=u["points"], + gold=u["gold"], silver=u["silver"], bronze=u["bronze"], + location=u["location"], occupation=u["occupation"], organization=u["organization"], + avatar_url=f"/static/images/avatars/{u['avatar']}.png", + created_at=_dt(u["joined"]), + )) + db.session.commit() + + +def seed_users(): + if User.query.filter_by(username="psi_grandmaster").first(): + return + for u in NOTABLE_USERS: + db.session.add(User( + email=f"{u['username']}@kaggle.test", username=u["username"], + password_hash=bcrypt.generate_password_hash("TestPass123!").decode(), + display_name=u["display_name"], bio=u["bio"], tier=u["tier"], + tiers_json=json.dumps(u["tiers"]), points=u["points"], + gold=u["gold"], silver=u["silver"], bronze=u["bronze"], + comp_rank=u.get("comp_rank"), + location=u["location"], occupation=u["occupation"], organization=u["organization"], + avatar_url=f"/static/images/avatars/{u['avatar']}.png", + is_org=u.get("is_org", False), created_at=_dt(u["joined"]), + )) + # The platform "Kaggle" host account. + db.session.add(User( + email="staff@kaggle.test", username="Kaggle", + password_hash=bcrypt.generate_password_hash("TestPass123!").decode(), + display_name="Kaggle", bio="Official Kaggle account.", tier="Grandmaster", + tiers_json=json.dumps({"competitions": "Grandmaster", "datasets": "Grandmaster", + "notebooks": "Grandmaster", "discussions": "Grandmaster"}), + points=0, gold=0, silver=0, bronze=0, is_org=True, + avatar_url="/static/images/avatars/kaggle.png", created_at=_dt("2010-04-01"), + )) + db.session.commit() + + +def seed_competitions(): + if Competition.query.count() > 0: + return + for c in COMPETITIONS: + db.session.add(Competition( + slug=c["slug"], title=c["title"], subtitle=c["subtitle"], + category=c["category"], host=c["host"], owner_username=c["owner"], + reward=c["reward"], reward_value=c["reward_value"], metric=c["metric"], + num_teams=c["num_teams"], deadline=_d(c["deadline"]), + tags_json=json.dumps(c["tags"]), thumbnail=c["thumbnail"], + description=c["description"], + )) + db.session.commit() + for slug, rows in LEADERBOARDS.items(): + comp = Competition.query.filter_by(slug=slug).first() + if not comp: + continue + for rank, team, uname, score, submitted in rows: + u = User.query.filter_by(username=uname).first() + db.session.add(Submission( + competition_id=comp.id, rank=rank, team_name=team, + user_id=u.id if u else None, score=score, submitted_at=_d(submitted), + )) + db.session.commit() + + +def seed_datasets(): + if Dataset.query.count() > 0: + return + for d in DATASETS: + db.session.add(Dataset( + slug=d["slug"], title=d["title"], subtitle=d["subtitle"], + owner_username=d["owner"], size=d["size"], size_bytes=d["size_bytes"], + file_count=d["file_count"], file_types=d["file_types"], usability=d["usability"], + upvotes=d["upvotes"], downloads=d["downloads"], views=d["views"], + license=d["license"], tags_json=json.dumps(d["tags"]), + last_updated=_d(d["last_updated"]), thumbnail=d["thumbnail"], + description=d["description"], + )) + db.session.commit() + + +def seed_notebooks(): + if Notebook.query.count() > 0: + return + for n in NOTEBOOKS: + db.session.add(Notebook( + slug=n["slug"], title=n["title"], author_username=n["author"], + language=n["language"], votes=n["votes"], comments=n["comments"], + medal=n["medal"], best_score=n["best_score"], runtime=n["runtime"], + last_run=_d(n["last_run"]), linked_competition=n["linked_competition"], + linked_dataset=n["linked_dataset"], thumbnail=n["thumbnail"], + tags_json=json.dumps(n["tags"]), description=n["description"], + )) + db.session.commit() + + +def seed_models(): + if Model.query.count() > 0: + return + for m in MODELS: + db.session.add(Model( + slug=m["slug"], title=m["title"], owner_username=m["owner"], + framework=m["framework"], variations=m["variations"], downloads=m["downloads"], + upvotes=m["upvotes"], license=m["license"], tags_json=json.dumps(m["tags"]), + last_updated=_d(m["last_updated"]), thumbnail=m["thumbnail"], + description=m["description"], + )) + db.session.commit() + + +def seed_courses(): + if Course.query.count() > 0: + return + for c in COURSES: + db.session.add(Course( + slug=c["slug"], title=c["title"], lessons=c["lessons"], hours=c["hours"], + level=c["level"], icon=c["icon"], tags_json=json.dumps(c["tags"]), + description=c["description"], lesson_titles_json=json.dumps(c["lesson_titles"]), + )) + db.session.commit() + + +def seed_discussions(): + if Discussion.query.count() > 0: + return + for d in DISCUSSIONS: + db.session.add(Discussion( + slug=d["slug"], title=d["title"], author_username=d["author"], + forum=d["forum"], votes=d["votes"], comment_count=d["comments"], + pinned=d["pinned"], created_at=_dt(d["created_at"]), body=d["body"], + )) + db.session.commit() + for slug, rows in DISCUSSION_COMMENTS.items(): + disc = Discussion.query.filter_by(slug=slug).first() + if not disc: + continue + for uname, body, votes, created in rows: + db.session.add(Comment(discussion_id=disc.id, author_username=uname, + body=body, votes=votes, created_at=_dt(created))) + db.session.commit() + + +def seed_benchmark_data(): + """Give benchmark users pre-existing saved items, joined competitions, and + follows so their account pages feel populated. Gated as a whole for + idempotency. Deliberately avoids every tasks.jsonl target so the benchmark + tasks stay valid (e.g. alice does NOT pre-join 'llm-prompt-recovery').""" + alice = User.query.filter_by(email="alice.j@test.com").first() + if not alice: + return + if Bookmark.query.filter_by(user_id=alice.id).first(): + return # already seeded + + def bk(u, etype, slug, model): + ent = model.query.filter_by(slug=slug).first() + if u and ent: + db.session.add(Bookmark(user_id=u.id, entity_type=etype, entity_id=ent.id)) + + def join(u, slug, team): + c = Competition.query.filter_by(slug=slug).first() + if u and c: + db.session.add(CompetitionEntry(user_id=u.id, competition_id=c.id, + team_name=team, accepted_rules=True, + joined_at=mirror_now())) + + bob = User.query.filter_by(email="bob.c@test.com").first() + carol = User.query.filter_by(email="carol.d@test.com").first() + + bk(alice, "notebook", "co2-emissions-trends-eda", Notebook) + bk(alice, "dataset", "spotify-tracks-audio-features", Dataset) + join(alice, "titanic-survival", "alicejdata") + db.session.add(Follow(user_id=alice.id, target_username="datasmith_io")) + + bk(bob, "dataset", "student-performance-factors", Dataset) + join(bob, "handwritten-digit-recognizer", "bobsmith_ml") + + bk(carol, "model", "resnet50-chestxray", Model) + db.session.add(Follow(user_id=carol.id, target_username="kenji_cv")) + db.session.commit() + + +with app.app_context(): + db.create_all() + seed_benchmark_users() + seed_users() + seed_competitions() + seed_datasets() + seed_notebooks() + seed_models() + seed_courses() + seed_discussions() + seed_benchmark_data() + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 5000)) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/kaggle/requirements.txt b/sites/kaggle/requirements.txt new file mode 100644 index 00000000..ede23c8f --- /dev/null +++ b/sites/kaggle/requirements.txt @@ -0,0 +1,7 @@ +Flask==3.1.0 +Flask-SQLAlchemy==3.1.1 +Flask-Login==0.6.3 +Flask-WTF==1.2.2 +Flask-Bcrypt==1.0.1 +WTForms==3.2.1 +email-validator==2.2.0 diff --git a/sites/kaggle/seed_data.py b/sites/kaggle/seed_data.py new file mode 100644 index 00000000..a2e7e09a --- /dev/null +++ b/sites/kaggle/seed_data.py @@ -0,0 +1,890 @@ +"""Kaggle mirror — seed catalog data. + +Plain Python data structures consumed by app.py's idempotent seed_*() functions. +No SQLAlchemy / Flask imports here so this module can be inspected standalone. + +Kaggle's domain entities: + User — competitors with progression tiers + performance medals + Competition — hosted ML challenges with prizes, leaderboards, deadlines + Dataset — community-uploaded datasets with usability scores + Notebook — public code (kernels), Python/R, optionally linked to a dataset + Model — pretrained models with framework + variations + Course — Kaggle Learn micro-courses + Discussion — forum threads across category forums + +Everything here is deterministic — no randomness at module import or seed time, +so the byte-identical-reset invariant holds. +""" + +# ---------------------------------------------------------------------------- +# Controlled vocabularies +# ---------------------------------------------------------------------------- +PERFORMANCE_TIERS = ["Novice", "Contributor", "Expert", "Master", "Grandmaster"] + +COMPETITION_CATEGORIES = [ + "Featured", "Research", "Getting Started", "Playground", + "Community", "Analytics", +] + +DISCUSSION_FORUMS = [ + "General", "Getting Started", "Product Feedback", + "Questions & Answers", "Competition Hosting", "Datasets", +] + +ML_FRAMEWORKS = ["PyTorch", "TensorFlow", "JAX", "Keras", "scikit-learn", "Transformers"] + +LICENSES = [ + "CC0: Public Domain", "CC BY-SA 4.0", "CC BY 4.0", "MIT", + "Apache 2.0", "GPL 2", "Other (specified in description)", + "Database: Open Database, Contents: Database Contents", +] + +PROGRAMMING_LANGUAGES = ["Python", "R"] + +# ---------------------------------------------------------------------------- +# Users — competitors. `tiers` maps the four ranked categories to a tier. +# medals are lifetime (gold, silver, bronze) performance medals. +# The first four (alice/bob/carol/david) are the standard WebHarbor benchmark +# accounts; the rest are notable-style Grandmasters for catalog depth. +# ---------------------------------------------------------------------------- +BENCHMARK_USERS = [ + { + "email": "alice.j@test.com", "username": "alicejdata", "password": "TestPass123!", + "display_name": "Alice Johnson", "tier": "Expert", + "tiers": {"competitions": "Expert", "datasets": "Master", "notebooks": "Expert", "discussions": "Contributor"}, + "points": 48210, "gold": 1, "silver": 6, "bronze": 14, + "location": "Seattle, United States", "occupation": "Data Scientist", "organization": "Cascadia Analytics", + "bio": "ML practitioner focused on tabular modeling and feature engineering. Datasets Master.", + "joined": "2019-03-11", "avatar": "alicejdata", + }, + { + "email": "bob.c@test.com", "username": "bobsmith_ml", "password": "TestPass123!", + "display_name": "Bob Smith", "tier": "Contributor", + "tiers": {"competitions": "Contributor", "datasets": "Contributor", "notebooks": "Novice", "discussions": "Novice"}, + "points": 9120, "gold": 0, "silver": 1, "bronze": 5, + "location": "Manchester, United Kingdom", "occupation": "Student", "organization": "University of Manchester", + "bio": "Learning the ropes — mostly Getting Started competitions and Kaggle Learn.", + "joined": "2022-09-02", "avatar": "bobsmith_ml", + }, + { + "email": "carol.d@test.com", "username": "carolwong", "password": "TestPass123!", + "display_name": "Carol Wong", "tier": "Master", + "tiers": {"competitions": "Master", "datasets": "Expert", "notebooks": "Master", "discussions": "Expert"}, + "points": 102450, "gold": 4, "silver": 11, "bronze": 9, + "location": "Singapore", "occupation": "Research Engineer", "organization": "DeepReef AI", + "bio": "Computer vision and deep learning. Two-time competition gold medalist.", + "joined": "2017-06-20", "avatar": "carolwong", + }, + { + "email": "david.k@test.com", "username": "davidtran", "password": "TestPass123!", + "display_name": "David Tran", "tier": "Novice", + "tiers": {"competitions": "Novice", "datasets": "Novice", "notebooks": "Novice", "discussions": "Novice"}, + "points": 320, "gold": 0, "silver": 0, "bronze": 0, + "location": "Toronto, Canada", "occupation": "Software Engineer", "organization": "", + "bio": "Just joined Kaggle to learn data science.", + "joined": "2026-05-30", "avatar": "davidtran", + }, +] + +NOTABLE_USERS = [ + { + "username": "psi_grandmaster", "display_name": "Priya Sharma", "tier": "Grandmaster", + "tiers": {"competitions": "Grandmaster", "datasets": "Master", "notebooks": "Grandmaster", "discussions": "Master"}, + "points": 312400, "gold": 19, "silver": 22, "bronze": 17, "comp_rank": 3, + "location": "Bengaluru, India", "occupation": "Principal Data Scientist", "organization": "Helios Labs", + "bio": "Competitions Grandmaster. Gradient boosting, stacking, and relentless cross-validation.", + "joined": "2014-08-01", "avatar": "psi_grandmaster", + }, + { + "username": "kenji_cv", "display_name": "Kenji Watanabe", "tier": "Grandmaster", + "tiers": {"competitions": "Grandmaster", "datasets": "Expert", "notebooks": "Master", "discussions": "Expert"}, + "points": 287600, "gold": 16, "silver": 25, "bronze": 12, "comp_rank": 7, + "location": "Tokyo, Japan", "occupation": "Senior ML Engineer", "organization": "Sakura Vision", + "bio": "Image segmentation and medical imaging specialist.", + "joined": "2015-02-14", "avatar": "kenji_cv", + }, + { + "username": "datasmith_io", "display_name": "Lena Fischer", "tier": "Grandmaster", + "tiers": {"competitions": "Master", "datasets": "Grandmaster", "notebooks": "Grandmaster", "discussions": "Grandmaster"}, + "points": 198750, "gold": 6, "silver": 14, "bronze": 31, "comp_rank": 41, + "location": "Berlin, Germany", "occupation": "Data Engineer", "organization": "Open Data Collective", + "bio": "Datasets & Notebooks Grandmaster. I publish clean, well-documented public datasets.", + "joined": "2016-11-30", "avatar": "datasmith_io", + }, + { + "username": "marco_nlp", "display_name": "Marco Rossi", "tier": "Master", + "tiers": {"competitions": "Master", "datasets": "Expert", "notebooks": "Master", "discussions": "Master"}, + "points": 121300, "gold": 3, "silver": 9, "bronze": 18, "comp_rank": 96, + "location": "Milan, Italy", "occupation": "NLP Researcher", "organization": "Lingua Systems", + "bio": "Natural language processing, transformers, and LLM fine-tuning.", + "joined": "2018-04-22", "avatar": "marco_nlp", + }, + { + "username": "sara_timeseries", "display_name": "Sara Okafor", "tier": "Master", + "tiers": {"competitions": "Master", "datasets": "Master", "notebooks": "Expert", "discussions": "Contributor"}, + "points": 88900, "gold": 2, "silver": 7, "bronze": 13, "comp_rank": 158, + "location": "Lagos, Nigeria", "occupation": "Quantitative Analyst", "organization": "Sahel Capital", + "bio": "Time-series forecasting and demand prediction.", + "joined": "2018-12-05", "avatar": "sara_timeseries", + }, + { + "username": "tomeka_viz", "display_name": "Tomeka Banks", "tier": "Expert", + "tiers": {"competitions": "Contributor", "datasets": "Expert", "notebooks": "Grandmaster", "discussions": "Expert"}, + "points": 76400, "gold": 1, "silver": 4, "bronze": 22, "comp_rank": 402, + "location": "Atlanta, United States", "occupation": "Data Visualization Lead", "organization": "ClearChart", + "bio": "Notebooks Grandmaster. I make EDA notebooks people actually read.", + "joined": "2019-07-18", "avatar": "tomeka_viz", + }, + { + "username": "raul_gbm", "display_name": "Raúl Mendoza", "tier": "Grandmaster", + "tiers": {"competitions": "Grandmaster", "datasets": "Contributor", "notebooks": "Expert", "discussions": "Master"}, + "points": 256100, "gold": 12, "silver": 19, "bronze": 21, "comp_rank": 12, + "location": "Mexico City, Mexico", "occupation": "Kaggle Competitions Grandmaster", "organization": "", + "bio": "Full-time competitor. LightGBM, XGBoost, and a lot of coffee.", + "joined": "2015-09-09", "avatar": "raul_gbm", + }, + { + "username": "hosting_org_zindi", "display_name": "Global Health Data Initiative", "tier": "Contributor", + "tiers": {"competitions": "Contributor", "datasets": "Master", "notebooks": "Novice", "discussions": "Contributor"}, + "points": 14200, "gold": 0, "silver": 2, "bronze": 6, "comp_rank": None, + "location": "Geneva, Switzerland", "occupation": "Competition Host", "organization": "GHDI", + "bio": "Non-profit hosting public-health ML challenges.", + "joined": "2020-01-15", "avatar": "hosting_org_zindi", "is_org": True, + }, +] + +# ---------------------------------------------------------------------------- +# Competitions +# Each: slug, title, subtitle, category, host (org name), reward (display), +# reward_value (numeric USD for sorting; 0 for non-cash), metric, num_teams, +# deadline (ISO), tags, thumbnail key, description, owner_username (host acct). +# active=True means deadline in the future relative to the mirror clock. +# ---------------------------------------------------------------------------- +COMPETITIONS = [ + { + "slug": "spaceship-titanic-rescue", + "title": "Spaceship Titanic: Predict the Rescued", + "subtitle": "Predict which passengers were transported to an alternate dimension", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "Classification Accuracy", "num_teams": 2841, "deadline": "2027-01-01", + "tags": ["binary classification", "tabular", "beginner"], "thumbnail": "spaceship-titanic-rescue", + "owner": "Kaggle", + "description": "A beginner-friendly classification challenge. Given passenger records from the interstellar liner Spaceship Titanic, predict whether each passenger was transported to an alternate dimension during the disaster. Perfect for learning feature engineering and model validation.", + }, + { + "slug": "house-prices-advanced-regression", + "title": "House Prices: Advanced Regression Techniques", + "subtitle": "Predict final sale prices of residential homes in Ames, Iowa", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "RMSE (log scale)", "num_teams": 4602, "deadline": "2027-01-01", + "tags": ["regression", "tabular", "feature engineering"], "thumbnail": "house-prices-advanced-regression", + "owner": "Kaggle", + "description": "With 79 explanatory variables describing almost every aspect of residential homes, predict the final sale price. A classic regression competition that rewards careful feature engineering and ensembling.", + }, + { + "slug": "rsna-pneumonia-detection-2026", + "title": "RSNA Pneumonia Detection Challenge 2026", + "subtitle": "Detect pneumonia in chest radiographs", + "category": "Featured", "host": "Radiological Society of North America", "reward": "$30,000", "reward_value": 30000, + "metric": "Mean Average Precision (mAP)", "num_teams": 1187, "deadline": "2026-09-15", + "tags": ["computer vision", "medical imaging", "object detection"], "thumbnail": "rsna-pneumonia-detection-2026", + "owner": "kenji_cv", + "description": "Build an algorithm to detect a visual signal for pneumonia in medical images. Each image may contain zero, one, or several bounding-box regions of opacity. Evaluation is mean average precision at multiple IoU thresholds.", + }, + { + "slug": "global-wheat-yield-forecast", + "title": "Global Wheat Yield Forecast", + "subtitle": "Forecast regional wheat yields from satellite and weather data", + "category": "Research", "host": "FAO Agricultural Data Lab", "reward": "$75,000", "reward_value": 75000, + "metric": "Root Mean Squared Error", "num_teams": 624, "deadline": "2026-08-01", + "tags": ["regression", "time series", "geospatial", "climate"], "thumbnail": "global-wheat-yield-forecast", + "owner": "sara_timeseries", + "description": "Forecast end-of-season wheat yields for agricultural regions worldwide using multi-temporal satellite imagery, soil records, and weather station data. A research competition aimed at improving food-security planning.", + }, + { + "slug": "llm-prompt-recovery", + "title": "LLM Prompt Recovery", + "subtitle": "Recover the prompt used to transform a piece of text", + "category": "Featured", "host": "Kaggle", "reward": "$50,000", "reward_value": 50000, + "metric": "Mean Sharpened Cosine Similarity", "num_teams": 2210, "deadline": "2026-07-10", + "tags": ["nlp", "llm", "text"], "thumbnail": "llm-prompt-recovery", + "owner": "marco_nlp", + "description": "Given an original text and its rewritten version, recover the natural-language instruction (prompt) that an LLM was given to perform the rewrite. Submissions are scored by sharpened cosine similarity between predicted and ground-truth prompt embeddings.", + }, + { + "slug": "store-sales-demand-forecasting", + "title": "Store Sales — Time Series Forecasting", + "subtitle": "Forecast grocery sales for thousands of product families", + "category": "Playground", "host": "Kaggle", "reward": "Swag", "reward_value": 0, + "metric": "Root Mean Squared Logarithmic Error", "num_teams": 1893, "deadline": "2026-12-31", + "tags": ["time series", "regression", "retail"], "thumbnail": "store-sales-demand-forecasting", + "owner": "Kaggle", + "description": "Predict unit sales for thousands of items sold at different stores. Practice time-series feature engineering with promotions, holidays, and oil-price covariates. Monthly Playground-style challenge.", + }, + { + "slug": "malaria-cell-classification", + "title": "Malaria Cell Image Classification", + "subtitle": "Classify blood-smear cell images as infected or healthy", + "category": "Featured", "host": "Global Health Data Initiative", "reward": "$25,000", "reward_value": 25000, + "metric": "ROC AUC", "num_teams": 944, "deadline": "2026-06-30", + "tags": ["computer vision", "medical imaging", "binary classification", "health"], "thumbnail": "malaria-cell-classification", + "owner": "hosting_org_zindi", + "description": "Classify segmented red-blood-cell images from thin blood smears as parasitized or uninfected. Hosted by a non-profit to accelerate low-cost malaria screening in the field.", + }, + { + "slug": "nyc-taxi-fare-prediction", + "title": "New York City Taxi Fare Prediction", + "subtitle": "Predict the fare of a NYC taxi ride", + "category": "Playground", "host": "Kaggle", "reward": "Swag", "reward_value": 0, + "metric": "Root Mean Squared Error", "num_teams": 1402, "deadline": "2026-11-20", + "tags": ["regression", "tabular", "geospatial"], "thumbnail": "nyc-taxi-fare-prediction", + "owner": "Kaggle", + "description": "Predict taxi fare amounts in New York City from pickup/dropoff coordinates, timestamps, and passenger counts. A great playground for geospatial feature engineering.", + }, + { + "slug": "credit-default-risk-2026", + "title": "Home Credit Default Risk 2026", + "subtitle": "Predict how capable each applicant is of repaying a loan", + "category": "Featured", "host": "Home Credit Group", "reward": "$100,000", "reward_value": 100000, + "metric": "ROC AUC", "num_teams": 3308, "deadline": "2026-10-05", + "tags": ["tabular", "binary classification", "finance"], "thumbnail": "credit-default-risk-2026", + "owner": "psi_grandmaster", + "description": "Use historical application, credit-bureau, and installment data to predict loan-repayment ability for applicants with little or no credit history. The largest cash prize on the platform this season.", + }, + { + "slug": "leaf-disease-segmentation", + "title": "Plant Leaf Disease Segmentation", + "subtitle": "Segment diseased regions on crop-leaf photographs", + "category": "Research", "host": "FAO Agricultural Data Lab", "reward": "$40,000", "reward_value": 40000, + "metric": "Dice Coefficient", "num_teams": 511, "deadline": "2026-09-28", + "tags": ["computer vision", "segmentation", "agriculture"], "thumbnail": "leaf-disease-segmentation", + "owner": "kenji_cv", + "description": "Pixel-level segmentation of disease lesions on leaf images across 12 crop species. Helps build smartphone tools for early disease detection by smallholder farmers.", + }, + { + "slug": "titanic-survival", + "title": "Titanic — Machine Learning from Disaster", + "subtitle": "The legendary starter competition: predict survival on the Titanic", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "Classification Accuracy", "num_teams": 14820, "deadline": "2027-01-01", + "tags": ["binary classification", "tabular", "beginner"], "thumbnail": "titanic-survival", + "owner": "Kaggle", + "description": "The most popular Getting Started competition. Use passenger data (name, age, sex, class, fare) to predict who survived the 1912 Titanic shipwreck. Your first stop on Kaggle.", + }, + { + "slug": "retail-customer-churn-analytics", + "title": "Retail Customer Churn Analytics", + "subtitle": "Analytics challenge: explain and predict subscriber churn", + "category": "Analytics", "host": "Streamline Retail", "reward": "$15,000", "reward_value": 15000, + "metric": "Judged (report quality)", "num_teams": 287, "deadline": "2026-07-31", + "tags": ["analytics", "tabular", "business"], "thumbnail": "retail-customer-churn-analytics", + "owner": "tomeka_viz", + "description": "An analytics competition judged on the quality of your written analysis and visualizations, not a leaderboard metric. Identify the key drivers of customer churn and recommend retention strategies.", + }, + { + "slug": "arctic-sea-ice-forecast", + "title": "Arctic Sea Ice Extent Forecast", + "subtitle": "Forecast monthly Arctic sea-ice extent", + "category": "Research", "host": "Polar Climate Consortium", "reward": "$60,000", "reward_value": 60000, + "metric": "Mean Absolute Error", "num_teams": 398, "deadline": "2026-08-20", + "tags": ["time series", "climate", "regression", "geospatial"], "thumbnail": "arctic-sea-ice-forecast", + "owner": "sara_timeseries", + "description": "Forecast pan-Arctic and regional sea-ice extent up to six months ahead from satellite passive-microwave records and reanalysis climate fields. A research competition supporting climate science.", + }, + { + "slug": "handwritten-digit-recognizer", + "title": "Digit Recognizer", + "subtitle": "Learn computer vision fundamentals with the famous MNIST data", + "category": "Getting Started", "host": "Kaggle", "reward": "Knowledge", "reward_value": 0, + "metric": "Classification Accuracy", "num_teams": 5210, "deadline": "2027-01-01", + "tags": ["computer vision", "image classification", "beginner"], "thumbnail": "handwritten-digit-recognizer", + "owner": "Kaggle", + "description": "Identify handwritten digits 0–9 from 28×28 grayscale images (MNIST). The standard introduction to image classification on Kaggle.", + }, + { + "slug": "fraud-detection-stream", + "title": "Real-Time Fraud Detection", + "subtitle": "Flag fraudulent transactions in a streaming feed", + "category": "Featured", "host": "PayServe", "reward": "$80,000", "reward_value": 80000, + "metric": "PR AUC", "num_teams": 1605, "deadline": "2026-06-26", + "tags": ["tabular", "binary classification", "finance", "imbalanced"], "thumbnail": "fraud-detection-stream", + "owner": "raul_gbm", + "description": "Detect fraudulent card transactions in a highly imbalanced stream where fewer than 0.2% of records are fraud. Scored by area under the precision-recall curve. Closes soon.", + }, + { + "slug": "sentiment-of-product-reviews", + "title": "Sentiment of Product Reviews", + "subtitle": "Playground NLP: classify review sentiment into five stars", + "category": "Playground", "host": "Kaggle", "reward": "Swag", "reward_value": 0, + "metric": "Quadratic Weighted Kappa", "num_teams": 1021, "deadline": "2026-12-15", + "tags": ["nlp", "text", "ordinal classification"], "thumbnail": "sentiment-of-product-reviews", + "owner": "marco_nlp", + "description": "Predict the 1–5 star rating implied by the text of an online product review. A monthly Playground competition for practicing text classification and ordinal targets.", + }, + { + "slug": "energy-load-forecasting", + "title": "City Energy Load Forecasting", + "subtitle": "Forecast hourly electricity demand for a metropolitan grid", + "category": "Research", "host": "GridSense Energy", "reward": "$45,000", "reward_value": 45000, + "metric": "Mean Absolute Percentage Error", "num_teams": 472, "deadline": "2026-10-30", + "tags": ["time series", "regression", "energy"], "thumbnail": "energy-load-forecasting", + "owner": "sara_timeseries", + "description": "Forecast hourly electricity load for a large city grid using weather forecasts, calendar features, and historical consumption. Helps utilities plan generation and reduce waste.", + }, +] + +# Leaderboard submissions per competition slug. Each tuple: +# (rank, team_name, owner_username, score, submitted_at). Scores ordered by rank. +LEADERBOARDS = { + "credit-default-risk-2026": [ + (1, "Gradient Surfers", "psi_grandmaster", 0.81342, "2026-06-18"), + (2, "Boosted Beavers", "raul_gbm", 0.81197, "2026-06-19"), + (3, "Reef Net", "carolwong", 0.80955, "2026-06-15"), + (4, "Data Mavens", "datasmith_io", 0.80610, "2026-06-17"), + (5, "alicejdata", "alicejdata", 0.80288, "2026-06-16"), + (6, "Mendoza Solo", "marco_nlp", 0.79944, "2026-06-12"), + ], + "fraud-detection-stream": [ + (1, "Boosted Beavers", "raul_gbm", 0.91205, "2026-06-21"), + (2, "Gradient Surfers", "psi_grandmaster", 0.90880, "2026-06-20"), + (3, "Sahel Signals", "sara_timeseries", 0.89712, "2026-06-19"), + (4, "alicejdata", "alicejdata", 0.88450, "2026-06-18"), + ], + "rsna-pneumonia-detection-2026": [ + (1, "Sakura Vision", "kenji_cv", 0.26412, "2026-06-10"), + (2, "Reef Net", "carolwong", 0.25988, "2026-06-14"), + (3, "PixelMedics", "tomeka_viz", 0.24501, "2026-06-09"), + ], + "llm-prompt-recovery": [ + (1, "Lingua Systems", "marco_nlp", 0.74129, "2026-06-20"), + (2, "Gradient Surfers", "psi_grandmaster", 0.73004, "2026-06-19"), + (3, "carolwong", "carolwong", 0.71880, "2026-06-17"), + ], +} + +# ---------------------------------------------------------------------------- +# Datasets +# slug, title, subtitle, owner_username, size (display), size_bytes (sort), +# file_count, file_types, usability (0-10 one decimal), upvotes, downloads, +# views, license, tags, last_updated (ISO), thumbnail, description +# ---------------------------------------------------------------------------- +DATASETS = [ + { + "slug": "global-co2-emissions-1960-2025", "owner": "datasmith_io", + "title": "Global CO₂ Emissions 1960–2025", + "subtitle": "Per-country annual CO₂ emissions, GDP, and population", + "size": "44 MB", "size_bytes": 46137344, "file_count": 3, "file_types": "CSV", + "usability": 10.0, "upvotes": 1842, "downloads": 39120, "views": 210400, + "license": "CC0: Public Domain", "tags": ["climate", "economics", "environment", "tabular"], + "last_updated": "2026-05-28", "thumbnail": "global-co2-emissions-1960-2025", + "description": "A tidy, country-level panel of annual CO₂ emissions (total and per-capita) joined with GDP and population from 1960 to 2025. Cleaned, de-duplicated, and ready for analysis.", + }, + { + "slug": "imdb-50k-movie-reviews", "owner": "marco_nlp", + "title": "IMDB 50K Movie Reviews", + "subtitle": "Balanced binary sentiment classification corpus", + "size": "66 MB", "size_bytes": 69206016, "file_count": 1, "file_types": "CSV", + "usability": 9.4, "upvotes": 3201, "downloads": 88210, "views": 402100, + "license": "Other (specified in description)", "tags": ["nlp", "text", "sentiment", "binary classification"], + "last_updated": "2025-11-12", "thumbnail": "imdb-50k-movie-reviews", + "description": "50,000 highly polar movie reviews labeled positive or negative, split 25k/25k for training and testing. The standard benchmark for binary sentiment classification.", + }, + { + "slug": "nyc-airbnb-2026", "owner": "alicejdata", + "title": "New York City Airbnb Listings 2026", + "subtitle": "Listing prices, locations, availability, and host details", + "size": "18 MB", "size_bytes": 18874368, "file_count": 2, "file_types": "CSV, GeoJSON", + "usability": 9.1, "upvotes": 1204, "downloads": 28740, "views": 150300, + "license": "CC BY-SA 4.0", "tags": ["geospatial", "tabular", "tourism", "pricing"], + "last_updated": "2026-04-02", "thumbnail": "nyc-airbnb-2026", + "description": "Every active Airbnb listing in NYC's five boroughs as of Q1 2026, with nightly price, room type, neighborhood, review counts, and host data. Includes a GeoJSON of neighborhood boundaries.", + }, + { + "slug": "chest-xray-pneumonia", "owner": "kenji_cv", + "title": "Chest X-Ray Images (Pneumonia)", + "subtitle": "5,863 labeled pediatric chest radiographs", + "size": "1.2 GB", "size_bytes": 1288490188, "file_count": 5863, "file_types": "JPEG", + "usability": 8.8, "upvotes": 5420, "downloads": 142300, "views": 610200, + "license": "CC BY 4.0", "tags": ["computer vision", "medical imaging", "health", "image classification"], + "last_updated": "2025-09-30", "thumbnail": "chest-xray-pneumonia", + "description": "Anterior-posterior chest X-ray images of pediatric patients, organized into NORMAL and PNEUMONIA folders for train/val/test. Widely used to benchmark medical-image classifiers.", + }, + { + "slug": "world-happiness-report-2026", "owner": "datasmith_io", + "title": "World Happiness Report 2026", + "subtitle": "National happiness scores and their six explanatory factors", + "size": "2 MB", "size_bytes": 2097152, "file_count": 1, "file_types": "CSV", + "usability": 10.0, "upvotes": 2987, "downloads": 61200, "views": 288700, + "license": "CC0: Public Domain", "tags": ["economics", "social science", "tabular", "survey"], + "last_updated": "2026-03-20", "thumbnail": "world-happiness-report-2026", + "description": "Ladder-of-life happiness scores for 150+ countries with the six contributing factors (GDP per capita, social support, healthy life expectancy, freedom, generosity, perceptions of corruption).", + }, + { + "slug": "credit-card-fraud-transactions", "owner": "raul_gbm", + "title": "Credit Card Fraud Transactions", + "subtitle": "Anonymized European card transactions, highly imbalanced", + "size": "150 MB", "size_bytes": 157286400, "file_count": 1, "file_types": "CSV", + "usability": 9.7, "upvotes": 8810, "downloads": 233400, "views": 901200, + "license": "Database: Open Database, Contents: Database Contents", "tags": ["finance", "tabular", "binary classification", "imbalanced"], + "last_updated": "2025-12-01", "thumbnail": "credit-card-fraud-transactions", + "description": "284,807 card transactions over two days, 492 of them fraudulent (0.172%). Features are PCA-transformed for privacy except Time and Amount. The canonical imbalanced-classification dataset.", + }, + { + "slug": "spotify-tracks-audio-features", "owner": "tomeka_viz", + "title": "Spotify Tracks — Audio Features", + "subtitle": "114k tracks with danceability, energy, valence, and more", + "size": "20 MB", "size_bytes": 20971520, "file_count": 1, "file_types": "CSV", + "usability": 9.4, "upvotes": 2110, "downloads": 47800, "views": 199600, + "license": "CC0: Public Domain", "tags": ["music", "tabular", "audio", "eda"], + "last_updated": "2026-01-18", "thumbnail": "spotify-tracks-audio-features", + "description": "114,000 Spotify tracks across 125 genres with audio features (tempo, energy, danceability, valence, acousticness) and popularity. Great for EDA, clustering, and recommendation.", + }, + { + "slug": "global-wheat-satellite-imagery", "owner": "sara_timeseries", + "title": "Global Wheat Satellite Imagery", + "subtitle": "Multi-temporal Sentinel-2 patches over wheat-growing regions", + "size": "3.4 GB", "size_bytes": 3650722201, "file_count": 21840, "file_types": "GeoTIFF, CSV", + "usability": 8.5, "upvotes": 742, "downloads": 9120, "views": 51200, + "license": "CC BY 4.0", "tags": ["geospatial", "computer vision", "agriculture", "climate", "time series"], + "last_updated": "2026-05-10", "thumbnail": "global-wheat-satellite-imagery", + "description": "Cloud-free Sentinel-2 image patches sampled across global wheat belts at five growth stages, paired with end-of-season yield labels. The companion dataset to the Global Wheat Yield Forecast competition.", + }, + { + "slug": "us-used-car-listings-2026", "owner": "alicejdata", + "title": "US Used Car Listings 2026", + "subtitle": "400k listings with price, mileage, make, model, and condition", + "size": "92 MB", "size_bytes": 96468992, "file_count": 1, "file_types": "CSV", + "usability": 9.2, "upvotes": 1533, "downloads": 35600, "views": 162900, + "license": "CC BY-SA 4.0", "tags": ["tabular", "pricing", "regression", "automotive"], + "last_updated": "2026-04-25", "thumbnail": "us-used-car-listings-2026", + "description": "Roughly 400,000 used-car listings scraped from US dealer sites in early 2026, with asking price, odometer, year, make, model, trim, fuel type, and condition. A solid regression playground.", + }, + { + "slug": "handwritten-digits-mnist", "owner": "Kaggle", + "title": "MNIST Handwritten Digits", + "subtitle": "70,000 labeled 28×28 grayscale digit images", + "size": "11 MB", "size_bytes": 11534336, "file_count": 4, "file_types": "CSV, IDX", + "usability": 10.0, "upvotes": 4290, "downloads": 178200, "views": 720100, + "license": "CC0: Public Domain", "tags": ["computer vision", "image classification", "beginner"], + "last_updated": "2025-08-14", "thumbnail": "handwritten-digits-mnist", + "description": "The classic MNIST dataset of handwritten digits 0–9 as flattened pixel CSVs and original IDX files. The 'hello world' of computer vision.", + }, + { + "slug": "amazon-product-reviews-electronics", "owner": "marco_nlp", + "title": "Amazon Product Reviews — Electronics", + "subtitle": "1.2M reviews with star ratings and helpfulness votes", + "size": "480 MB", "size_bytes": 503316480, "file_count": 1, "file_types": "JSON", + "usability": 8.9, "upvotes": 2670, "downloads": 58900, "views": 244300, + "license": "Other (specified in description)", "tags": ["nlp", "text", "sentiment", "ecommerce"], + "last_updated": "2025-10-22", "thumbnail": "amazon-product-reviews-electronics", + "description": "1.2 million electronics-category product reviews with 1–5 star ratings, review text, summary, and helpfulness votes. Use for sentiment, ordinal regression, or recommendation.", + }, + { + "slug": "world-cities-population", "owner": "datasmith_io", + "title": "World Cities Population & Coordinates", + "subtitle": "47k cities with population, country, and lat/long", + "size": "6 MB", "size_bytes": 6291456, "file_count": 1, "file_types": "CSV", + "usability": 9.8, "upvotes": 1920, "downloads": 51000, "views": 233100, + "license": "CC BY 4.0", "tags": ["geospatial", "tabular", "reference"], + "last_updated": "2026-02-09", "thumbnail": "world-cities-population", + "description": "47,000 world cities with population estimates, country and admin region, time zone, and latitude/longitude. A handy reference table for joining and mapping.", + }, + { + "slug": "retail-store-sales-history", "owner": "sara_timeseries", + "title": "Retail Store Sales History", + "subtitle": "Five years of daily sales across 54 stores and 33 families", + "size": "120 MB", "size_bytes": 125829120, "file_count": 6, "file_types": "CSV", + "usability": 9.3, "upvotes": 1340, "downloads": 31200, "views": 142800, + "license": "CC0: Public Domain", "tags": ["time series", "retail", "regression", "tabular"], + "last_updated": "2026-05-15", "thumbnail": "retail-store-sales-history", + "description": "Daily unit sales for 54 stores and 33 product families over five years, with promotions, holidays, oil prices, and store metadata. Companion data for the Store Sales forecasting competition.", + }, + { + "slug": "plant-leaf-disease-images", "owner": "kenji_cv", + "title": "Plant Leaf Disease Images", + "subtitle": "54k annotated leaf photos across 12 crop species", + "size": "2.1 GB", "size_bytes": 2254857830, "file_count": 54306, "file_types": "JPEG, PNG masks", + "usability": 8.7, "upvotes": 1108, "downloads": 17400, "views": 78200, + "license": "CC BY-SA 4.0", "tags": ["computer vision", "segmentation", "agriculture", "image classification"], + "last_updated": "2026-05-05", "thumbnail": "plant-leaf-disease-images", + "description": "54,306 leaf images spanning healthy and 26 disease classes across 12 crops, each with a pixel mask of the diseased region. Companion to the Plant Leaf Disease Segmentation competition.", + }, + { + "slug": "global-temperature-anomalies", "owner": "datasmith_io", + "title": "Global Temperature Anomalies 1880–2025", + "subtitle": "Monthly land+ocean temperature anomalies", + "size": "4 MB", "size_bytes": 4194304, "file_count": 2, "file_types": "CSV", + "usability": 9.9, "upvotes": 2240, "downloads": 49800, "views": 211000, + "license": "CC0: Public Domain", "tags": ["climate", "time series", "environment", "tabular"], + "last_updated": "2026-03-01", "thumbnail": "global-temperature-anomalies", + "description": "Monthly global land and ocean temperature anomalies relative to the 1951–1980 baseline, 1880 to present. Tidy and ready for time-series and trend analysis.", + }, + { + "slug": "student-performance-factors", "owner": "bobsmith_ml", + "title": "Student Performance Factors", + "subtitle": "Exam scores with study habits and background features", + "size": "1 MB", "size_bytes": 1048576, "file_count": 1, "file_types": "CSV", + "usability": 9.0, "upvotes": 980, "downloads": 22100, "views": 96500, + "license": "CC BY 4.0", "tags": ["education", "tabular", "regression", "beginner"], + "last_updated": "2026-06-01", "thumbnail": "student-performance-factors", + "description": "Synthetic-but-realistic records of student exam scores with study hours, attendance, parental involvement, sleep, and tutoring. A clean beginner regression dataset.", + }, +] + +# ---------------------------------------------------------------------------- +# Notebooks (Kernels / Code) +# slug, title, author_username, language, votes, comments, medal (or None), +# best_score (display or None), runtime, last_run (ISO), linked_competition, +# linked_dataset, thumbnail, description, tags +# ---------------------------------------------------------------------------- +NOTEBOOKS = [ + { + "slug": "eda-credit-default-deep-dive", "author": "psi_grandmaster", + "title": "Home Credit — Full EDA & Feature Factory", + "language": "Python", "votes": 1842, "comments": 214, "medal": "gold", + "best_score": None, "runtime": "428.6s", "last_run": "2026-06-15", + "linked_competition": "credit-default-risk-2026", "linked_dataset": None, + "thumbnail": "eda-credit-default-deep-dive", "tags": ["eda", "feature engineering", "lightgbm", "tabular"], + "description": "An end-to-end exploratory analysis of the Home Credit Default Risk data plus a reusable feature-engineering pipeline that joins all auxiliary tables and produces 700+ aggregated features.", + }, + { + "slug": "lgbm-baseline-fraud", "author": "raul_gbm", + "title": "LightGBM Baseline for Real-Time Fraud Detection", + "language": "Python", "votes": 1204, "comments": 138, "medal": "gold", + "best_score": "0.91205", "runtime": "92.4s", "last_run": "2026-06-21", + "linked_competition": "fraud-detection-stream", "linked_dataset": "credit-card-fraud-transactions", + "thumbnail": "lgbm-baseline-fraud", "tags": ["lightgbm", "imbalanced", "tabular", "baseline"], + "description": "A clean, well-commented LightGBM baseline for the Real-Time Fraud Detection competition, with class-weighting, threshold tuning on the PR curve, and out-of-fold validation. Reproduces a 0.912 leaderboard score.", + }, + { + "slug": "unet-pneumonia-segmentation", "author": "kenji_cv", + "title": "U-Net Pneumonia Detection — Training Pipeline", + "language": "Python", "votes": 932, "comments": 96, "medal": "gold", + "best_score": "0.264", "runtime": "3h 12m", "last_run": "2026-06-10", + "linked_competition": "rsna-pneumonia-detection-2026", "linked_dataset": "chest-xray-pneumonia", + "thumbnail": "unet-pneumonia-segmentation", "tags": ["pytorch", "computer vision", "segmentation", "medical imaging"], + "description": "A PyTorch U-Net with an EfficientNet encoder for the RSNA Pneumonia Detection Challenge, including augmentation, mixed-precision training, and a mAP evaluation harness.", + }, + { + "slug": "titanic-top-3-percent", "author": "carolwong", + "title": "Titanic — Top 3% Solution Walkthrough", + "language": "Python", "votes": 2610, "comments": 311, "medal": "gold", + "best_score": "0.81100", "runtime": "44.1s", "last_run": "2026-05-22", + "linked_competition": "titanic-survival", "linked_dataset": None, + "thumbnail": "titanic-top-3-percent", "tags": ["beginner", "feature engineering", "ensemble", "tabular"], + "description": "A friendly, fully explained Titanic solution that reaches the top 3% of the leaderboard with thoughtful feature engineering (titles, family size, fare bins) and a soft-voting ensemble.", + }, + { + "slug": "spotify-genre-clustering", "author": "tomeka_viz", + "title": "Spotify Audio Features — Clustering & Viz", + "language": "Python", "votes": 884, "comments": 73, "medal": "silver", + "best_score": None, "runtime": "61.2s", "last_run": "2026-02-01", + "linked_competition": None, "linked_dataset": "spotify-tracks-audio-features", + "thumbnail": "spotify-genre-clustering", "tags": ["eda", "clustering", "visualization", "music"], + "description": "Interactive UMAP + K-Means clustering of 114k Spotify tracks by audio features, with beautiful Plotly charts that reveal how genres separate in feature space.", + }, + { + "slug": "imdb-sentiment-transformers", "author": "marco_nlp", + "title": "IMDB Sentiment with DistilBERT (94% acc)", + "language": "Python", "votes": 1410, "comments": 152, "medal": "gold", + "best_score": "0.9412", "runtime": "27m 8s", "last_run": "2025-11-15", + "linked_competition": None, "linked_dataset": "imdb-50k-movie-reviews", + "thumbnail": "imdb-sentiment-transformers", "tags": ["nlp", "transformers", "pytorch", "sentiment"], + "description": "Fine-tunes DistilBERT on the IMDB 50K reviews dataset to 94% test accuracy, with a clean Hugging Face Trainer loop, learning-rate schedule, and confusion-matrix analysis.", + }, + { + "slug": "house-prices-stacked-regression", "author": "alicejdata", + "title": "House Prices — Stacked Regression (Top 5%)", + "language": "Python", "votes": 1190, "comments": 134, "medal": "silver", + "best_score": "0.11892", "runtime": "118.7s", "last_run": "2026-04-30", + "linked_competition": "house-prices-advanced-regression", "linked_dataset": None, + "thumbnail": "house-prices-stacked-regression", "tags": ["regression", "stacking", "feature engineering", "tabular"], + "description": "A stacked ensemble (Lasso, ElasticNet, Gradient Boosting, XGBoost) with careful skew correction and target encoding that lands in the top 5% of the House Prices leaderboard.", + }, + { + "slug": "r-timeseries-store-sales", "author": "sara_timeseries", + "title": "Store Sales Forecasting in R (fable + tsibble)", + "language": "R", "votes": 612, "comments": 58, "medal": "silver", + "best_score": "0.41203", "runtime": "204.9s", "last_run": "2026-05-18", + "linked_competition": "store-sales-demand-forecasting", "linked_dataset": "retail-store-sales-history", + "thumbnail": "r-timeseries-store-sales", "tags": ["r", "time series", "forecasting", "tidyverse"], + "description": "A tidyverts (fable + tsibble) workflow for the Store Sales competition in R: seasonal decomposition, exponential smoothing, and an ensemble of ETS and ARIMA per product family.", + }, + { + "slug": "mnist-cnn-from-scratch", "author": "bobsmith_ml", + "title": "MNIST CNN from Scratch (99.2% acc)", + "language": "Python", "votes": 421, "comments": 39, "medal": "bronze", + "best_score": "0.99214", "runtime": "12m 3s", "last_run": "2026-06-03", + "linked_competition": "handwritten-digit-recognizer", "linked_dataset": "handwritten-digits-mnist", + "thumbnail": "mnist-cnn-from-scratch", "tags": ["computer vision", "keras", "beginner", "cnn"], + "description": "A beginner-friendly Keras CNN for MNIST that reaches 99.2% with data augmentation and dropout, explained layer by layer.", + }, + { + "slug": "co2-emissions-trends-eda", "author": "datasmith_io", + "title": "Global CO₂ Emissions — Trends & Storytelling", + "language": "Python", "votes": 1056, "comments": 91, "medal": "silver", + "best_score": None, "runtime": "38.5s", "last_run": "2026-05-29", + "linked_competition": None, "linked_dataset": "global-co2-emissions-1960-2025", + "thumbnail": "co2-emissions-trends-eda", "tags": ["eda", "climate", "visualization", "storytelling"], + "description": "A narrative EDA of 65 years of global CO₂ emissions: who emits the most per capita, how emissions track GDP, and which countries have decoupled growth from carbon.", + }, + { + "slug": "wheat-yield-lgbm-geospatial", "author": "sara_timeseries", + "title": "Wheat Yield — Geospatial Features + LightGBM", + "language": "Python", "votes": 388, "comments": 44, "medal": "bronze", + "best_score": "1.0423", "runtime": "266.0s", "last_run": "2026-05-12", + "linked_competition": "global-wheat-yield-forecast", "linked_dataset": "global-wheat-satellite-imagery", + "thumbnail": "wheat-yield-lgbm-geospatial", "tags": ["geospatial", "lightgbm", "feature engineering", "agriculture"], + "description": "Extracts NDVI time-series and weather aggregates from the satellite patches, then trains a LightGBM regressor to forecast end-of-season wheat yield. Strong public-LB baseline.", + }, + { + "slug": "happiness-report-regression", "author": "bobsmith_ml", + "title": "What Makes Countries Happy? A Regression Study", + "language": "Python", "votes": 503, "comments": 47, "medal": "bronze", + "best_score": None, "runtime": "21.7s", "last_run": "2026-03-22", + "linked_competition": None, "linked_dataset": "world-happiness-report-2026", + "thumbnail": "happiness-report-regression", "tags": ["eda", "regression", "social science", "beginner"], + "description": "Explores which factors best predict national happiness using linear and tree models, with partial-dependence plots that quantify the contribution of GDP, social support, and freedom.", + }, +] + +# ---------------------------------------------------------------------------- +# Models +# slug, title, owner_username, framework, variations, downloads, upvotes, +# license, tags, last_updated, thumbnail, description +# ---------------------------------------------------------------------------- +MODELS = [ + { + "slug": "resnet50-chestxray", "owner": "kenji_cv", + "title": "ResNet-50 Chest X-Ray Classifier", + "framework": "PyTorch", "variations": 3, "downloads": 41200, "upvotes": 612, + "license": "Apache 2.0", "tags": ["computer vision", "medical imaging", "classification"], + "last_updated": "2026-04-18", "thumbnail": "resnet50-chestxray", + "description": "A ResNet-50 fine-tuned on the Chest X-Ray Pneumonia dataset, with fp16 and int8 variations for edge deployment. Reaches 0.97 ROC AUC on the held-out test split.", + }, + { + "slug": "distilbert-imdb-sentiment", "owner": "marco_nlp", + "title": "DistilBERT IMDB Sentiment", + "framework": "Transformers", "variations": 2, "downloads": 88700, "upvotes": 901, + "license": "MIT", "tags": ["nlp", "sentiment", "text classification"], + "last_updated": "2025-11-16", "thumbnail": "distilbert-imdb-sentiment", + "description": "DistilBERT fine-tuned on IMDB 50K reviews for binary sentiment, 94% accuracy. Includes base and quantized ONNX variations.", + }, + { + "slug": "lightgbm-fraud-detector", "owner": "raul_gbm", + "title": "LightGBM Fraud Detector", + "framework": "scikit-learn", "variations": 1, "downloads": 22300, "upvotes": 388, + "license": "Apache 2.0", "tags": ["tabular", "finance", "classification", "imbalanced"], + "last_updated": "2026-06-21", "thumbnail": "lightgbm-fraud-detector", + "description": "A serialized LightGBM model + preprocessing pipeline for card-fraud scoring, trained on the Credit Card Fraud Transactions dataset. PR-AUC 0.91.", + }, + { + "slug": "unet-leaf-segmentation", "owner": "kenji_cv", + "title": "U-Net Leaf Disease Segmenter", + "framework": "PyTorch", "variations": 2, "downloads": 9800, "upvotes": 201, + "license": "CC BY 4.0", "tags": ["computer vision", "segmentation", "agriculture"], + "last_updated": "2026-05-06", "thumbnail": "unet-leaf-segmentation", + "description": "U-Net with a ResNet-34 encoder trained on the Plant Leaf Disease Images dataset for pixel-level lesion segmentation. Dice 0.88 on validation.", + }, + { + "slug": "tabnet-credit-risk", "owner": "psi_grandmaster", + "title": "TabNet Credit Risk Model", + "framework": "PyTorch", "variations": 1, "downloads": 15600, "upvotes": 277, + "license": "Apache 2.0", "tags": ["tabular", "finance", "classification", "deep learning"], + "last_updated": "2026-06-16", "thumbnail": "tabnet-credit-risk", + "description": "An attention-based TabNet model for the Home Credit Default Risk problem, with feature-importance masks for interpretability. ROC AUC 0.80.", + }, + { + "slug": "prophet-energy-forecaster", "owner": "sara_timeseries", + "title": "Prophet City Energy Forecaster", + "framework": "scikit-learn", "variations": 1, "downloads": 7400, "upvotes": 142, + "license": "MIT", "tags": ["time series", "energy", "forecasting"], + "last_updated": "2026-05-20", "thumbnail": "prophet-energy-forecaster", + "description": "A tuned Prophet model with custom holiday and weather regressors for hourly metropolitan electricity-load forecasting. MAPE 3.1% on the validation horizon.", + }, + { + "slug": "efficientnet-leaf-classifier", "owner": "datasmith_io", + "title": "EfficientNet-B3 Plant Leaf Classifier", + "framework": "TensorFlow", "variations": 2, "downloads": 12800, "upvotes": 188, + "license": "Apache 2.0", "tags": ["computer vision", "agriculture", "image classification"], + "last_updated": "2026-03-28", "thumbnail": "efficientnet-leaf-classifier", + "description": "An EfficientNet-B3 image classifier trained on the Plant Leaf Disease Images dataset to recognise 26 disease classes across 12 crops. TensorFlow SavedModel and TFLite variations included.", + }, + { + "slug": "yolov8-traffic-detector", "owner": "carolwong", + "title": "YOLOv8 Urban Traffic Object Detector", + "framework": "PyTorch", "variations": 3, "downloads": 33100, "upvotes": 421, + "license": "GPL 2", "tags": ["computer vision", "object detection", "geospatial"], + "last_updated": "2026-06-02", "thumbnail": "yolov8-traffic-detector", + "description": "A YOLOv8 detector fine-tuned to spot cars, buses, cyclists, and pedestrians in dashcam and CCTV frames. Nano, small, and medium variations for different latency budgets.", + }, + { + "slug": "bert-ner-finance", "owner": "marco_nlp", + "title": "BERT Financial NER", + "framework": "Transformers", "variations": 1, "downloads": 19400, "upvotes": 256, + "license": "MIT", "tags": ["nlp", "named entity recognition", "finance"], + "last_updated": "2026-04-09", "thumbnail": "bert-ner-finance", + "description": "A BERT-base token-classification model that tags companies, tickers, monetary amounts, and dates in financial news and filings.", + }, + { + "slug": "xgboost-churn-predictor", "owner": "psi_grandmaster", + "title": "XGBoost Customer Churn Predictor", + "framework": "scikit-learn", "variations": 1, "downloads": 10250, "upvotes": 174, + "license": "Apache 2.0", "tags": ["tabular", "business", "classification"], + "last_updated": "2026-05-31", "thumbnail": "xgboost-churn-predictor", + "description": "A gradient-boosted churn classifier with SHAP explanations, trained on telco-style subscriber data. Ships with a calibrated probability head.", + }, +] + +# ---------------------------------------------------------------------------- +# Kaggle Learn courses +# slug, title, lessons, hours, level, icon, tags, description, lesson_titles +# ---------------------------------------------------------------------------- +COURSES = [ + { + "slug": "intro-to-machine-learning", "title": "Intro to Machine Learning", + "lessons": 7, "hours": 3, "level": "Beginner", "icon": "🤖", + "tags": ["machine learning", "beginner", "scikit-learn"], + "description": "Learn the core ideas in machine learning and build your first models. Decision trees, model validation, underfitting vs overfitting, and random forests.", + "lesson_titles": ["How Models Work", "Basic Data Exploration", "Your First Machine Learning Model", + "Model Validation", "Underfitting and Overfitting", "Random Forests", "Machine Learning Competitions"], + }, + { + "slug": "pandas", "title": "Pandas", + "lessons": 6, "hours": 4, "level": "Beginner", "icon": "🐼", + "tags": ["pandas", "data manipulation", "python"], + "description": "Solve short hands-on challenges to perfect your data-manipulation skills with pandas — the most important Python library for working with tabular data.", + "lesson_titles": ["Creating, Reading and Writing", "Indexing, Selecting & Assigning", "Summary Functions and Maps", + "Grouping and Sorting", "Data Types and Missing Values", "Renaming and Combining"], + }, + { + "slug": "intro-to-deep-learning", "title": "Intro to Deep Learning", + "lessons": 6, "hours": 4, "level": "Intermediate", "icon": "🧠", + "tags": ["deep learning", "keras", "neural networks"], + "description": "Use TensorFlow and Keras to build and train neural networks for structured data. Stochastic gradient descent, overfitting, dropout, and batch normalization.", + "lesson_titles": ["A Single Neuron", "Deep Neural Networks", "Stochastic Gradient Descent", + "Overfitting and Underfitting", "Dropout and Batch Normalization", "Binary Classification"], + }, + { + "slug": "computer-vision", "title": "Computer Vision", + "lessons": 6, "hours": 4, "level": "Intermediate", "icon": "👁️", + "tags": ["computer vision", "cnn", "keras", "deep learning"], + "description": "Build convolutional neural networks with TensorFlow and Keras. Learn about convolution, pooling, data augmentation, and transfer learning for image classification.", + "lesson_titles": ["The Convolutional Classifier", "Convolution and ReLU", "Maximum Pooling", + "The Sliding Window", "Custom Convnets", "Data Augmentation"], + }, + { + "slug": "feature-engineering", "title": "Feature Engineering", + "lessons": 6, "hours": 5, "level": "Intermediate", "icon": "🛠️", + "tags": ["feature engineering", "machine learning", "tabular"], + "description": "Better features make better models. Discover how to engineer features, measure mutual information, create features, and use target encoding and clustering.", + "lesson_titles": ["What Is Feature Engineering", "Mutual Information", "Creating Features", + "Clustering With K-Means", "Principal Component Analysis", "Target Encoding"], + }, + { + "slug": "intro-to-sql", "title": "Intro to SQL", + "lessons": 6, "hours": 3, "level": "Beginner", "icon": "🗄️", + "tags": ["sql", "bigquery", "data"], + "description": "Learn SQL for working with databases using Google BigQuery. SELECT, GROUP BY, ORDER BY, joins, and how to keep your queries under control.", + "lesson_titles": ["Getting Started With SQL and BigQuery", "Select, From & Where", "Group By, Having & Count", + "Order By", "As & With", "Joining Data"], + }, + { + "slug": "data-visualization", "title": "Data Visualization", + "lessons": 7, "hours": 4, "level": "Beginner", "icon": "📊", + "tags": ["visualization", "seaborn", "eda"], + "description": "Make great data visualizations with seaborn. Line, bar, and scatter charts, distributions, and choosing the right plot for your data and story.", + "lesson_titles": ["Hello, Seaborn", "Line Charts", "Bar Charts and Heatmaps", "Scatter Plots", + "Distributions", "Choosing Plot Types and Custom Styles", "Final Project"], + }, + { + "slug": "intro-to-deep-learning-nlp", "title": "Natural Language Processing", + "lessons": 4, "hours": 3, "level": "Advanced", "icon": "💬", + "tags": ["nlp", "transformers", "text", "deep learning"], + "description": "Distinguish yourself by learning to work with text data. Text classification, word vectors, and building NLP pipelines with spaCy and transformers.", + "lesson_titles": ["Intro to NLP", "Text Classification", "Word Vectors", "Transformers and Beyond"], + }, + { + "slug": "time-series", "title": "Time Series", + "lessons": 6, "hours": 4, "level": "Intermediate", "icon": "📈", + "tags": ["time series", "forecasting", "regression"], + "description": "Apply machine learning to real-world forecasting tasks. Trend and seasonality, time-series features, hybrid models, and forecasting with ML.", + "lesson_titles": ["Linear Regression With Time Series", "Trend", "Seasonality", + "Time Series as Features", "Hybrid Models", "Forecasting With Machine Learning"], + }, + { + "slug": "intro-to-ai-ethics", "title": "Intro to AI Ethics", + "lessons": 5, "hours": 4, "level": "Beginner", "icon": "⚖️", + "tags": ["ai ethics", "fairness", "responsible ai"], + "description": "Explore practical tools to guide the moral design of AI systems. Human-centered design, bias, fairness, model cards, and AI fairness metrics.", + "lesson_titles": ["Introduction", "Human-Centered Design for AI", "Identifying Bias in AI", + "AI Fairness", "Model Cards"], + }, +] + +# ---------------------------------------------------------------------------- +# Discussions (forum threads) +# slug, title, author_username, forum, votes, comments, pinned, created_at, body +# ---------------------------------------------------------------------------- +DISCUSSIONS = [ + { + "slug": "welcome-to-kaggle-getting-started", "author": "Kaggle", + "title": "Welcome to Kaggle! Start here", "forum": "Getting Started", + "votes": 5210, "comments": 642, "pinned": True, "created_at": "2025-01-05", + "body": "New to Kaggle? This thread links the essentials: pick a Getting Started competition like Titanic, take the Intro to Machine Learning course on Kaggle Learn, and don't be afraid to fork a public notebook. Welcome aboard!", + }, + { + "slug": "fraud-detection-pr-auc-vs-roc-auc", "author": "raul_gbm", + "title": "Why PR-AUC, not ROC-AUC, for the fraud comp?", "forum": "Questions & Answers", + "votes": 412, "comments": 58, "pinned": False, "created_at": "2026-06-08", + "body": "With fraud at 0.17% prevalence, ROC-AUC looks great even for weak models because true negatives dominate. PR-AUC focuses on the positive (fraud) class, so it's the honest metric here. Curious how others are calibrating their thresholds.", + }, + { + "slug": "credit-default-leak-warning", "author": "psi_grandmaster", + "title": "Heads up: potential target leak via SK_ID ordering", "forum": "Competition Hosting", + "votes": 388, "comments": 71, "pinned": True, "created_at": "2026-06-11", + "body": "If you sort by SK_ID_CURR you'll notice the default rate drifts. That's an artifact of how the data was exported, not signal. Using row order as a feature will overfit the public LB and collapse on private. Don't do it.", + }, + { + "slug": "best-gpu-for-kaggle-2026", "author": "bobsmith_ml", + "title": "Best free GPU setup for training in 2026?", "forum": "General", + "votes": 244, "comments": 89, "pinned": False, "created_at": "2026-05-19", + "body": "Kaggle gives 30 GPU hours/week on T4s and P100s. For the chest X-ray comp that's tight. Anyone splitting training across sessions or using gradient checkpointing to fit U-Net at higher resolution?", + }, + { + "slug": "feature-request-dark-mode", "author": "tomeka_viz", + "title": "Feature request: dark mode for notebooks", "forum": "Product Feedback", + "votes": 1820, "comments": 203, "pinned": False, "created_at": "2026-04-12", + "body": "Please ship a real dark mode for the notebook editor. My eyes during a 2am competition crunch would appreciate it. Upvote if you agree!", + }, + { + "slug": "sharing-co2-dataset-v3", "author": "datasmith_io", + "title": "[Dataset] Global CO₂ Emissions updated through 2025", "forum": "Datasets", + "votes": 302, "comments": 41, "pinned": False, "created_at": "2026-05-28", + "body": "Just pushed v3 of the Global CO₂ Emissions dataset with 2025 figures and a cleaned per-capita column. Usability is back to 10.0. Let me know if you spot any country-code mismatches.", + }, + { + "slug": "wheat-yield-ndvi-features", "author": "sara_timeseries", + "title": "What NDVI aggregation works best for wheat yield?", "forum": "Questions & Answers", + "votes": 156, "comments": 34, "pinned": False, "created_at": "2026-05-13", + "body": "I'm getting the best CV from max-NDVI in the heading stage plus the integral of NDVI over the season. Mean NDVI alone underperforms. What growth-stage windows are working for you?", + }, + { + "slug": "titanic-feature-ideas", "author": "carolwong", + "title": "Underrated Titanic features that actually help", "forum": "Getting Started", + "votes": 921, "comments": 167, "pinned": False, "created_at": "2026-05-22", + "body": "Beyond the obvious: extracting the title from Name (Mr/Mrs/Master/Rare), family size = SibSp+Parch+1, and a 'deck' letter from the cabin all move the needle. Ticket-group survival is the sneaky strong one.", + }, +] + +# Comments attached to discussions. slug -> list of (author_username, body, votes, created_at) +DISCUSSION_COMMENTS = { + "fraud-detection-pr-auc-vs-roc-auc": [ + ("psi_grandmaster", "Exactly. I tune the threshold to maximize F1 on out-of-fold predictions, then sanity-check the PR curve.", 41, "2026-06-08"), + ("alicejdata", "Class weights in LightGBM plus PR-AUC early stopping got me to 0.88. Thanks for the writeup!", 22, "2026-06-09"), + ], + "titanic-feature-ideas": [ + ("bobsmith_ml", "The title extraction alone bumped me from 0.76 to 0.79. Wild how much a regex helps.", 58, "2026-05-23"), + ("davidtran", "Total beginner here — thank you, the family-size feature finally got me off 0.62.", 12, "2026-05-24"), + ], +} diff --git a/sites/kaggle/static/css/.gitkeep b/sites/kaggle/static/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/static/css/main.css b/sites/kaggle/static/css/main.css new file mode 100644 index 00000000..802f83cf --- /dev/null +++ b/sites/kaggle/static/css/main.css @@ -0,0 +1,208 @@ +/* Kaggle mirror — styling approximating kaggle.com's Material-ish look. */ +:root { + --kaggle-cyan: #20beff; + --kaggle-cyan-dark: #1ba7e0; + --ink: #202124; + --ink-soft: #5f6368; + --line: #e0e0e0; + --bg: #ffffff; + --bg-soft: #f5f5f5; + --bg-rail: #fafafa; + --gold: #dca917; + --silver: #b5b5b5; + --bronze: #c08851; + --radius: 12px; + --shadow: 0 1px 2px rgba(60,64,67,.1), 0 1px 3px rgba(60,64,67,.08); + --shadow-hover: 0 2px 6px rgba(60,64,67,.18), 0 1px 4px rgba(60,64,67,.12); + --font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: var(--font); + color: var(--ink); + background: var(--bg); + font-size: 14px; + line-height: 1.5; +} +a { color: var(--kaggle-cyan-dark); text-decoration: none; } +a:hover { text-decoration: underline; } +img { max-width: 100%; } +h1, h2, h3, h4 { color: var(--ink); margin: 0 0 .5em; font-weight: 600; } + +.container { max-width: 1080px; margin: 0 auto; padding: 0 24px; } +.container-wide { max-width: 1280px; margin: 0 auto; padding: 0 24px; } + +/* ---------- Top nav ---------- */ +.gn { position: sticky; top: 0; z-index: 50; background: #fff; border-bottom: 1px solid var(--line); height: 56px; } +.gn-inner { max-width: 1280px; margin: 0 auto; padding: 0 20px; height: 56px; display: flex; align-items: center; gap: 16px; } +.gn-logo { display: flex; align-items: center; gap: 6px; font-weight: 700; font-size: 18px; color: var(--ink); } +.gn-logo:hover { text-decoration: none; } +.gn-logo svg { height: 26px; width: auto; display: block; } +.gn-search { flex: 1; max-width: 520px; } +.gn-search input { + width: 100%; height: 38px; border: 1px solid var(--line); border-radius: 999px; + padding: 0 16px; font-size: 14px; background: var(--bg-soft); outline: none; +} +.gn-search input:focus { background: #fff; border-color: var(--kaggle-cyan); } +.gn-nav { display: flex; gap: 4px; align-items: center; } +.gn-nav a { color: var(--ink-soft); padding: 8px 10px; border-radius: 6px; font-weight: 500; } +.gn-nav a:hover { background: var(--bg-soft); color: var(--ink); text-decoration: none; } +.gn-auth { display: flex; gap: 8px; align-items: center; } +.gn-avatar img { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; border: 1px solid var(--line); display: block; } + +/* ---------- Buttons ---------- */ +.btn { display: inline-flex; align-items: center; gap: 6px; border: 1px solid transparent; border-radius: 999px; + padding: 8px 18px; font-size: 14px; font-weight: 600; cursor: pointer; font-family: inherit; } +.btn:hover { text-decoration: none; } +.btn-sm { padding: 6px 14px; font-size: 13px; } +.btn-primary { background: var(--kaggle-cyan); color: #fff; } +.btn-primary:hover { background: var(--kaggle-cyan-dark); color: #fff; } +.btn-ghost { background: transparent; color: var(--ink); border-color: var(--line); } +.btn-ghost:hover { background: var(--bg-soft); } +.btn-outline { background: #fff; color: var(--kaggle-cyan-dark); border-color: var(--kaggle-cyan); } +.btn-outline:hover { background: #eafaff; } +.btn-block { width: 100%; justify-content: center; } + +/* ---------- Flash ---------- */ +.flash-wrap { max-width: 1080px; margin: 12px auto 0; padding: 0 24px; } +.flash { padding: 10px 16px; border-radius: 8px; margin-bottom: 8px; font-weight: 500; } +.flash-success { background: #e6f8ee; color: #137333; border: 1px solid #b6e6c9; } +.flash-error { background: #fce8e6; color: #c5221f; border: 1px solid #f5c2bd; } +.flash-info { background: #e8f0fe; color: #1967d2; border: 1px solid #c2d7fb; } + +/* ---------- Hero ---------- */ +.hero { background: linear-gradient(120deg, #0b2c3d 0%, #114a63 50%, #1ba7e0 100%); color: #fff; padding: 56px 0; } +.hero h1 { color: #fff; font-size: 38px; margin-bottom: 12px; } +.hero p { font-size: 17px; color: #d6eefb; max-width: 640px; margin: 0 0 24px; } +.hero .btn-primary { background: #fff; color: #114a63; } +.hero .btn-primary:hover { background: #eafaff; } + +/* ---------- Sections ---------- */ +.section { padding: 36px 0; } +.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 18px; } +.section-head h2 { font-size: 22px; margin: 0; } +.section-head a { font-size: 14px; font-weight: 600; } + +/* ---------- Cards / grids ---------- */ +.grid { display: grid; gap: 16px; } +.grid-2 { grid-template-columns: repeat(2, 1fr); } +.grid-3 { grid-template-columns: repeat(3, 1fr); } +.grid-4 { grid-template-columns: repeat(4, 1fr); } +@media (max-width: 980px) { .grid-3, .grid-4 { grid-template-columns: repeat(2, 1fr); } } +@media (max-width: 620px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } } + +.card { background: #fff; border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); transition: box-shadow .15s, transform .15s; } +.card:hover { box-shadow: var(--shadow-hover); } +.card a { color: inherit; } +.card-thumb { width: 100%; height: 130px; object-fit: cover; background: var(--bg-soft); display: block; } +.card-thumb-tall { height: 160px; } +.card-body { padding: 14px 16px; } +.card-title { font-size: 15px; font-weight: 600; margin: 0 0 4px; line-height: 1.35; } +.card-sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 10px; } +.card-meta { display: flex; flex-wrap: wrap; gap: 12px; color: var(--ink-soft); font-size: 12.5px; } +.card-meta b { color: var(--ink); font-weight: 600; } + +/* gradient placeholder shown if a real thumbnail is missing */ +.thumb-fallback { display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; + font-size: 22px; background: linear-gradient(135deg, #20beff, #114a63); } + +/* ---------- Tags / chips ---------- */ +.chips { display: flex; flex-wrap: wrap; gap: 6px; } +.chip { display: inline-block; background: var(--bg-soft); color: var(--ink-soft); border-radius: 999px; + padding: 3px 10px; font-size: 12px; font-weight: 500; } +.chip-link:hover { background: #e3f4fc; color: var(--kaggle-cyan-dark); text-decoration: none; } +.chip-active { background: var(--kaggle-cyan); color: #fff; } + +/* ---------- Tier badge ---------- */ +.tier { display: inline-flex; align-items: center; gap: 5px; font-weight: 700; font-size: 12.5px; } +.tier-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; } +.medal { font-size: 13px; } +.medal-counts { display: inline-flex; gap: 10px; align-items: center; } + +/* ---------- List rows ---------- */ +.row-list { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; background: #fff; } +.row { display: flex; gap: 14px; padding: 14px 18px; border-bottom: 1px solid var(--line); align-items: center; } +.row:last-child { border-bottom: none; } +.row:hover { background: var(--bg-rail); } +.row-thumb { width: 56px; height: 56px; border-radius: 10px; object-fit: cover; flex-shrink: 0; background: var(--bg-soft); } +.row-main { flex: 1; min-width: 0; } +.row-title { font-size: 15px; font-weight: 600; margin: 0 0 2px; } +.row-sub { color: var(--ink-soft); font-size: 13px; margin: 0 0 6px; } +.row-right { text-align: right; color: var(--ink-soft); font-size: 13px; white-space: nowrap; } + +/* ---------- Filter bar ---------- */ +.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 18px; } +.toolbar form { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; } +.toolbar select, .toolbar input[type=text] { + height: 36px; border: 1px solid var(--line); border-radius: 8px; padding: 0 10px; font-size: 13px; background: #fff; font-family: inherit; } +.page-title { font-size: 26px; margin: 0 0 4px; } +.page-sub { color: var(--ink-soft); margin: 0 0 18px; } +.count-pill { color: var(--ink-soft); font-size: 13px; font-weight: 500; } + +/* ---------- Detail layout ---------- */ +.detail-head { display: flex; gap: 20px; align-items: flex-start; padding: 28px 0 20px; border-bottom: 1px solid var(--line); } +.detail-thumb { width: 110px; height: 110px; border-radius: 14px; object-fit: cover; flex-shrink: 0; background: var(--bg-soft); } +.detail-title { font-size: 28px; margin: 0 0 6px; } +.detail-sub { color: var(--ink-soft); font-size: 16px; margin: 0 0 12px; } +.detail-cols { display: grid; grid-template-columns: 1fr 300px; gap: 28px; padding: 24px 0; } +@media (max-width: 900px) { .detail-cols { grid-template-columns: 1fr; } } +.side-box { border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; margin-bottom: 16px; background: #fff; } +.side-box h4 { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); margin: 0 0 10px; } +.stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; } +.stat { } +.stat-num { font-size: 20px; font-weight: 700; } +.stat-label { color: var(--ink-soft); font-size: 12px; } +.prose { font-size: 15px; line-height: 1.7; color: #3c4043; } +.prose p { margin: 0 0 14px; } + +.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 8px 0 20px; } +.tabs a { padding: 10px 16px; color: var(--ink-soft); font-weight: 600; border-bottom: 2px solid transparent; } +.tabs a:hover { text-decoration: none; color: var(--ink); } +.tabs a.active { color: var(--kaggle-cyan-dark); border-bottom-color: var(--kaggle-cyan); } + +/* ---------- Tables ---------- */ +table.lb { width: 100%; border-collapse: collapse; font-size: 14px; } +table.lb th, table.lb td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); } +table.lb th { color: var(--ink-soft); font-size: 12px; text-transform: uppercase; letter-spacing: .03em; } +table.lb tr:hover td { background: var(--bg-rail); } +.rank-medal { font-weight: 700; } + +/* ---------- Forms ---------- */ +.form-card { max-width: 420px; margin: 48px auto; background: #fff; border: 1px solid var(--line); border-radius: var(--radius); padding: 32px; box-shadow: var(--shadow); } +.form-card.wide { max-width: 620px; } +.form-card h1 { font-size: 24px; text-align: center; margin-bottom: 8px; } +.form-card .sub { text-align: center; color: var(--ink-soft); margin-bottom: 24px; } +.field { margin-bottom: 16px; } +.field label { display: block; font-weight: 600; font-size: 13px; margin-bottom: 6px; } +.field input, .field textarea, .field select { + width: 100%; border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; font-size: 14px; font-family: inherit; } +.field input:focus, .field textarea:focus { outline: none; border-color: var(--kaggle-cyan); } +.field .err { color: #c5221f; font-size: 12px; margin-top: 4px; } +.muted { color: var(--ink-soft); } +.center { text-align: center; } + +/* ---------- Profile ---------- */ +.profile-head { display: flex; gap: 22px; align-items: center; padding: 28px 0; } +.profile-avatar { width: 110px; height: 110px; border-radius: 50%; object-fit: cover; border: 3px solid #fff; box-shadow: var(--shadow); } +.profile-name { font-size: 26px; margin: 0; } +.profile-handle { color: var(--ink-soft); font-size: 15px; } + +/* ---------- Vote button ---------- */ +.vote-btn { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); background: #fff; + border-radius: 999px; padding: 6px 14px; font-weight: 600; font-size: 13px; cursor: pointer; color: var(--ink); font-family: inherit; } +.vote-btn:hover { border-color: var(--kaggle-cyan); color: var(--kaggle-cyan-dark); } +.vote-btn.voted { background: #eafaff; border-color: var(--kaggle-cyan); color: var(--kaggle-cyan-dark); } + +/* ---------- Footer ---------- */ +.footer { border-top: 1px solid var(--line); margin-top: 48px; padding: 36px 0 28px; background: var(--bg-rail); } +.footer-cols { display: grid; grid-template-columns: 1.4fr repeat(3, 1fr); gap: 24px; } +@media (max-width: 760px) { .footer-cols { grid-template-columns: 1fr 1fr; } } +.footer h4 { font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--ink-soft); } +.footer a { display: block; color: var(--ink-soft); padding: 3px 0; font-size: 13px; } +.footer a:hover { color: var(--kaggle-cyan-dark); } +.footer-brand-logo { font-weight: 700; font-size: 18px; display: flex; align-items: center; gap: 6px; } +.footer-bottom { display: flex; justify-content: space-between; color: var(--ink-soft); font-size: 12.5px; margin-top: 24px; border-top: 1px solid var(--line); padding-top: 16px; } + +.empty { text-align: center; color: var(--ink-soft); padding: 48px 0; } diff --git a/sites/kaggle/static/icons/.gitkeep b/sites/kaggle/static/icons/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/static/js/.gitkeep b/sites/kaggle/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/static/js/main.js b/sites/kaggle/static/js/main.js new file mode 100644 index 00000000..84524193 --- /dev/null +++ b/sites/kaggle/static/js/main.js @@ -0,0 +1,60 @@ +// Kaggle mirror — lightweight interactions (vote / bookmark / follow via fetch). +(function () { + const csrf = document.querySelector('meta[name="csrf-token"]'); + const token = csrf ? csrf.getAttribute('content') : ''; + + function post(url, data) { + return fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRFToken': token, + }, + body: JSON.stringify(data), + }).then(r => r.json()); + } + + document.addEventListener('click', function (e) { + const vb = e.target.closest('[data-vote]'); + if (vb) { + e.preventDefault(); + post('/api/vote', { entity_type: vb.dataset.type, entity_id: vb.dataset.id }) + .then(res => { + if (res.error) { window.location = '/login'; return; } + vb.classList.toggle('voted', res.voted); + const cnt = vb.querySelector('[data-count]'); + if (cnt && res.count != null) cnt.textContent = res.count.toLocaleString(); + const lbl = vb.querySelector('[data-label]'); + if (lbl) lbl.textContent = res.voted ? 'Voted' : 'Upvote'; + }) + .catch(() => { window.location = '/login'; }); + return; + } + const bb = e.target.closest('[data-bookmark]'); + if (bb) { + e.preventDefault(); + post('/api/bookmark', { entity_type: bb.dataset.type, entity_id: bb.dataset.id }) + .then(res => { + if (res.error) { window.location = '/login'; return; } + bb.classList.toggle('voted', res.bookmarked); + const lbl = bb.querySelector('[data-label]'); + if (lbl) lbl.textContent = res.bookmarked ? 'Saved' : 'Save'; + }) + .catch(() => { window.location = '/login'; }); + return; + } + const fb = e.target.closest('[data-follow]'); + if (fb) { + e.preventDefault(); + post('/api/follow', { username: fb.dataset.username }) + .then(res => { + if (res.error) { window.location = '/login'; return; } + fb.classList.toggle('voted', res.following); + fb.textContent = res.following ? 'Following' : 'Follow'; + }) + .catch(() => { window.location = '/login'; }); + return; + } + }); +})(); diff --git a/sites/kaggle/tasks.jsonl b/sites/kaggle/tasks.jsonl new file mode 100644 index 00000000..fc9050a3 --- /dev/null +++ b/sites/kaggle/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name": "Kaggle", "id": "Kaggle--0", "ques": "Find the Getting Started competition about predicting passenger survival on the Titanic and open its page. What is the evaluation metric used?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_0.py", "judge_rubric": "PASS requires: opened the Titanic (passenger survival) Getting Started competition page and reported its evaluation metric = 'Classification Accuracy'. Answering without opening the competition page, or a wrong metric, = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--1", "ques": "Open the 'Home Credit Default Risk 2026' competition and report which evaluation metric is used to score submissions.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_1.py", "judge_rubric": "PASS requires: opened the 'Home Credit Default Risk 2026' competition and reported the scoring metric = 'ROC AUC'. Wrong metric or no navigation = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--2", "ques": "Among the datasets tagged 'climate', which one has the most upvotes? Give its title.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_2.py", "judge_rubric": "PASS requires: browsed climate-tagged datasets and named the most-upvoted one = 'Global Temperature Anomalies 1880–2025' (2240 upvotes). Naming a different climate dataset = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--3", "ques": "Find the 'Credit Card Fraud Transactions' dataset and download it.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_3.py", "judge_rubric": "PASS requires: opened the 'Credit Card Fraud Transactions' dataset and triggered its download (the download counter must increment). No download action = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--4", "ques": "On the 'Home Credit Default Risk 2026' competition leaderboard, which team is ranked #1 and what is their score?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_4.py", "judge_rubric": "PASS requires: opened the Home Credit Default Risk 2026 leaderboard and reported rank #1 = team 'Gradient Surfers' with score 0.81342. Wrong team or score = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--5", "ques": "Find a PyTorch model for chest X-ray classification and report the license it is released under.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_5.py", "judge_rubric": "PASS requires: opened the PyTorch chest X-ray model ('ResNet-50 Chest X-Ray Classifier') and reported its license = 'Apache 2.0'. Wrong license or no navigation = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--6", "ques": "Sign in with email alice.j@test.com and password TestPass123!, then join the 'LLM Prompt Recovery' competition with the team name 'Data Wizards'.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_6.py", "judge_rubric": "PASS requires: signed in as alice.j and joined the 'LLM Prompt Recovery' competition with team name 'Data Wizards'. No competition entry, or a different team name, = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--7", "ques": "Sign in with email alice.j@test.com and password TestPass123!, then upvote the dataset 'World Happiness Report 2026'.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_7.py", "judge_rubric": "PASS requires: signed in as alice.j and upvoted the 'World Happiness Report 2026' dataset (a vote must be recorded). No upvote = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--8", "ques": "Sign in with email bob.c@test.com and password TestPass123!, then save (bookmark) the notebook 'Titanic — Top 3% Solution Walkthrough' to your account.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_8.py", "judge_rubric": "PASS requires: signed in as bob.c and bookmarked/saved the notebook 'Titanic — Top 3% Solution Walkthrough' (a bookmark must be recorded). No bookmark = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--9", "ques": "Sign in with email bob.c@test.com and password TestPass123!, then follow the user 'psi_grandmaster'.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_9.py", "judge_rubric": "PASS requires: signed in as bob.c and followed the user 'psi_grandmaster' (a follow must be recorded). No follow = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--10", "ques": "Sign in with email alice.j@test.com and password TestPass123!, then start a new discussion in the 'Questions & Answers' forum titled 'How do you handle class imbalance?' with a short body.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_10.py", "judge_rubric": "PASS requires: signed in as alice.j and created a new discussion titled 'How do you handle class imbalance?' in the 'Questions & Answers' forum. No thread, wrong forum, or wrong author = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--11", "ques": "Sign in with email david.k@test.com and password TestPass123!, open the discussion 'Underrated Titanic features that actually help', and post a comment thanking the author.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_11.py", "judge_rubric": "PASS requires: signed in as david.k, opened the 'Underrated Titanic features that actually help' discussion, and posted a comment. No comment by david on that thread = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--12", "ques": "Sign in with email david.k@test.com and password TestPass123!, then update your profile location to 'Boston, United States'.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_12.py", "judge_rubric": "PASS requires: signed in as david.k and updated the profile location to 'Boston, United States'. Leaving the seed location (Toronto, Canada) = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--13", "ques": "Open the Notebooks rankings page. Who is the top-ranked user (rank #1)?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_13.py", "judge_rubric": "PASS requires: opened the Notebooks rankings page and named the rank #1 user = 'psi_grandmaster' (Priya Sharma). Wrong user or no navigation = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--14", "ques": "On Kaggle Learn, how many lessons does the 'Intro to Machine Learning' course contain?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_14.py", "judge_rubric": "PASS requires: opened the 'Intro to Machine Learning' Kaggle Learn course and reported it has 7 lessons. Wrong count or no navigation = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--15", "ques": "Open the 'Credit Card Fraud Transactions' dataset, find a public notebook that uses it, open that notebook, and report its best leaderboard score.", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_15.py", "judge_rubric": "PASS requires: opened the 'Credit Card Fraud Transactions' dataset, then opened the public notebook that uses it ('LightGBM Baseline for Real-Time Fraud Detection'), and reported its best leaderboard score = 0.91205. Missing either navigation or a wrong score = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--16", "ques": "Among the Featured competitions that offer a cash prize, which one has the largest reward, and how much is it?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_16.py", "judge_rubric": "PASS requires: browsed Featured competitions and reported the largest cash prize = 'Home Credit Default Risk 2026' at $100,000. Naming a different competition or amount = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--17", "ques": "Look at the competitions hosted by the user 'sara_timeseries'. Which of her hosted competitions has the earliest deadline?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_17.py", "judge_rubric": "PASS requires: opened sara_timeseries's profile and named her earliest-deadline hosted competition = 'Global Wheat Yield Forecast' (deadline 2026-08-01). Naming a different competition = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--18", "ques": "Open the 'MNIST Handwritten Digits' dataset. Under what license is it released?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_18.py", "judge_rubric": "PASS requires: opened the 'MNIST Handwritten Digits' dataset and reported its license = 'CC0: Public Domain'. Wrong license or no navigation = FAIL."} +{"web_name": "Kaggle", "id": "Kaggle--19", "ques": "Among Python notebooks that earned a gold medal, which one has the most votes, and who is its author?", "web": "http://localhost:40016/", "upstream_url": "https://www.kaggle.com/", "verifier_path": "sites/kaggle/verify/verify_19.py", "judge_rubric": "PASS requires: browsed Python gold-medal notebooks and named the most-voted one = 'Titanic — Top 3% Solution Walkthrough' by carolwong. Wrong notebook or author = FAIL."} diff --git a/sites/kaggle/templates/.gitkeep b/sites/kaggle/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/kaggle/templates/404.html b/sites/kaggle/templates/404.html new file mode 100644 index 00000000..70f7cd2e --- /dev/null +++ b/sites/kaggle/templates/404.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Not Found | Kaggle{% endblock %} +{% block content %} +
+

404

+

We couldn't find that page.

+ Back to Home +
+{% endblock %} diff --git a/sites/kaggle/templates/500.html b/sites/kaggle/templates/500.html new file mode 100644 index 00000000..9c84719a --- /dev/null +++ b/sites/kaggle/templates/500.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Error | Kaggle{% endblock %} +{% block content %} +
+

500

+

Something went wrong on our end.

+ Back to Home +
+{% endblock %} diff --git a/sites/kaggle/templates/_macros.html b/sites/kaggle/templates/_macros.html new file mode 100644 index 00000000..0bcfebf8 --- /dev/null +++ b/sites/kaggle/templates/_macros.html @@ -0,0 +1,89 @@ +{# Reusable card / badge macros #} +{% set TIER_COLORS = {"Novice": "#5ac995", "Contributor": "#00aaff", "Expert": "#95319b", "Master": "#f96517", "Grandmaster": "#dca917"} %} + +{% macro thumb(kind, key, title, cls='card-thumb') %} + {{ title }} +{% endmacro %} + +{% macro tier_badge(user) %} + {% if user %} + + {{ user.tier }} + + {% endif %} +{% endmacro %} + +{% macro tier_name(name) %} + + {{ name }} + +{% endmacro %} + +{% macro medals(g, s, b) %} + + 🥇 {{ g }} + 🥈 {{ s }} + 🥉 {{ b }} + +{% endmacro %} + +{% macro competition_card(c) %} + +{% endmacro %} + +{% macro dataset_card(d) %} + +{% endmacro %} + +{% macro notebook_card(n) %} + +{% endmacro %} + +{% macro model_card(m) %} + +{% endmacro %} diff --git a/sites/kaggle/templates/account.html b/sites/kaggle/templates/account.html new file mode 100644 index 00000000..14a374e6 --- /dev/null +++ b/sites/kaggle/templates/account.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_badge, medals %} +{% block title %}Your Account | Kaggle{% endblock %} +{% block content %} +
+
+ +
+

{{ current_user.display_name or current_user.username }} {{ tier_badge(current_user) }}

+
@{{ current_user.username }} · {{ current_user.email }}
+
{{ medals(current_user.gold, current_user.silver, current_user.bronze) }} · {{ current_user.points|commafy }} points
+
+ +
+ +
+

My Competitions ({{ entries|length }})

+ {% if entries %} +
{% for e in entries %} +
+ +
Team: {{ e.team_name }} · joined {{ e.joined_at|reltime }}
+
{{ e.competition.reward }}
+ {% endfor %}
+ {% else %}
You haven't joined any competitions yet. Browse competitions
{% endif %} +
+ +
+

Saved ({{ bookmarks|length }})

+ {% if bookmarks %} +
{% for ent, b in bookmarks %} +
+
{{ ent.title }}
+
{{ b.entity_type }}
+
+ {% endfor %}
+ {% else %}
No saved items yet.
{% endif %} +
+ +
+

Following ({{ follows|length }})

+ {% if follows %} +
{% for f in follows %}@{{ f.target_username }}{% endfor %}
+ {% else %}
You're not following anyone yet.
{% endif %} +
+ +
+
+ + +
+
+
+{% endblock %} diff --git a/sites/kaggle/templates/account_edit.html b/sites/kaggle/templates/account_edit.html new file mode 100644 index 00000000..1b6a3a61 --- /dev/null +++ b/sites/kaggle/templates/account_edit.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Edit Profile | Kaggle{% endblock %} +{% block content %} +
+

Edit Profile

+
+ {{ form.csrf_token }} +
{{ form.display_name() }}
+
{{ form.bio(rows=4) }}
+
{{ form.location() }}
+
{{ form.occupation() }}
+
{{ form.organization() }}
+
{{ form.website() }}
+ +
+

← Back to account

+
+{% endblock %} diff --git a/sites/kaggle/templates/base.html b/sites/kaggle/templates/base.html new file mode 100644 index 00000000..b3cbb160 --- /dev/null +++ b/sites/kaggle/templates/base.html @@ -0,0 +1,99 @@ + + + + + + + {% block title %}Kaggle: Your Machine Learning and Data Science Community{% endblock %} + + + + +{% macro kaggle_logo() %} + + + + + kaggle + +{% endmacro %} + + +{% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} +{% endwith %} + +{% block content %}{% endblock %} + + + + +{% block scripts %}{% endblock %} + + diff --git a/sites/kaggle/templates/change_password.html b/sites/kaggle/templates/change_password.html new file mode 100644 index 00000000..ffdb1dfa --- /dev/null +++ b/sites/kaggle/templates/change_password.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Change Password | Kaggle{% endblock %} +{% block content %} +
+

Change Password

+
+ {{ form.csrf_token }} +
{{ form.current() }} + {% for e in form.current.errors %}
{{ e }}
{% endfor %}
+
{{ form.new() }} + {% for e in form.new.errors %}
{{ e }}
{% endfor %}
+
{{ form.confirm() }} + {% for e in form.confirm.errors %}
{{ e }}
{% endfor %}
+ +
+

← Back to account

+
+{% endblock %} diff --git a/sites/kaggle/templates/competition_detail.html b/sites/kaggle/templates/competition_detail.html new file mode 100644 index 00000000..fd03048e --- /dev/null +++ b/sites/kaggle/templates/competition_detail.html @@ -0,0 +1,131 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ c.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('competitions', c.thumbnail, c.title, 'detail-thumb') }} +
+
{{ c.category }} + {% if c.is_active %}Active + {% else %}Closed{% endif %}
+

{{ c.title }}

+
{{ c.subtitle }}
+
+ Hosted by {{ c.host }} + {{ c.reward }} prize + {{ c.num_teams|commafy }} teams +
+
+
+ {% if joined %} + ✓ Joined as {{ entry.team_name }} + {% elif c.is_active %} + Join Competition + {% else %} + Competition closed + {% endif %} +
+
+ +
+ Overview + Leaderboard + Code + {% if c.is_active %}Join{% endif %} +
+ +
+
+ {% if tab == 'leaderboard' %} +

Leaderboard

+ {% if leaderboard %} + + + + {% for s in leaderboard %} + + + + + + + + {% endfor %} + +
#TeamMembersScoreSubmitted
{% if s.rank==1 %}🥇{% elif s.rank==2 %}🥈{% elif s.rank==3 %}🥉{% else %}{{ s.rank }}{% endif %}{{ s.team_name }}{% if user_by_name(s.team_name) %}{{ s.team_name }}{% else %}team{% endif %}{{ '%.5f'|format(s.score) }}{{ s.submitted_at|reltime }}
+ {% else %}
No public submissions yet.
{% endif %} + + {% elif tab == 'code' %} +

Notebooks

+ {% if notebooks %} +
+ {% for n in notebooks %} +
+
+ +
{{ n.author_username }} · {{ n.language }}{% if n.best_score %} · score {{ n.best_score }}{% endif %}
+
+
👍 {{ n.votes|commafy }}
+
+ {% endfor %} +
+ {% else %}
No public notebooks for this competition yet.
{% endif %} + + {% elif tab == 'join' and c.is_active %} +

Join {{ c.title }}

+ {% if current_user.is_authenticated %} + {% if joined %} +

You've already joined as team {{ entry.team_name }}. Good luck!

+ {% else %} +
+ {{ join_form.csrf_token }} +
+ + {{ join_form.team_name(placeholder="Your team name") }} +
+
+ +
+ +
+ {% endif %} + {% else %} +

You must sign in to join this competition.

+ {% endif %} + + {% else %} +

Overview

+
+ {% for para in c.description.split('\n') %}

{{ para }}

{% endfor %} +
+

Evaluation

+

Submissions are scored using {{ c.metric }}.

+ {% if c.tags %}
{% for t in c.tags %}{{ t }}{% endfor %}
{% endif %} + {% endif %} +
+ + +
+
+{% endblock %} diff --git a/sites/kaggle/templates/competitions.html b/sites/kaggle/templates/competitions.html new file mode 100644 index 00000000..f9eadfbc --- /dev/null +++ b/sites/kaggle/templates/competitions.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% from "_macros.html" import competition_card %} +{% block title %}Competitions | Kaggle{% endblock %} +{% block content %} +
+

Competitions

+

Grow your data science skills by competing in our ML competitions. Find help in the docs.

+ +
+
+ + + + + +
+ {{ comps|length }} competitions +
+ + {% if comps %} +
+ {% for c in comps %}{{ competition_card(c) }}{% endfor %} +
+ {% else %} +
No competitions match your filters.
+ {% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/course_detail.html b/sites/kaggle/templates/course_detail.html new file mode 100644 index 00000000..ce55155f --- /dev/null +++ b/sites/kaggle/templates/course_detail.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% block title %}{{ course.title }} | Kaggle Learn{% endblock %} +{% block content %} +
+
+
{{ course.icon }}
+
+

{{ course.title }}

+
{{ course.level }} · {{ course.lessons }} lessons · {{ course.hours }} hours
+
{% for t in course.tags %}{{ t }}{% endfor %}
+
+ +
+
+
+

About this course

+

{{ course.description }}

+

Lessons

+
+ {% for lt in course.lesson_titles %} +
+
{{ loop.index }}. {{ lt }}
+
Tutorial + Exercise
+ {% endfor %} +
+
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/dataset_detail.html b/sites/kaggle/templates/dataset_detail.html new file mode 100644 index 00000000..500ee3e0 --- /dev/null +++ b/sites/kaggle/templates/dataset_detail.html @@ -0,0 +1,72 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ d.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('datasets', d.thumbnail, d.title, 'detail-thumb') }} +
+

{{ d.title }}

+
{{ d.subtitle }}
+
+ {{ d.owner_username }} + Updated {{ d.last_updated|reltime }} + Usability {{ '%.1f'|format(d.usability) }} +
+
+
+ + + ⬇ Download ({{ d.size }}) +
+
+ +
+
+

About this Dataset

+
{% for para in d.description.split('\n') %}

{{ para }}

{% endfor %}
+ {% if d.tags %}
{% for t in d.tags %}{{ t }}{% endfor %}
{% endif %} + +

Notebooks using this dataset

+ {% if notebooks %} +
+ {% for n in notebooks %} +
+ +
{{ n.author_username }} · {{ n.language }}
+
👍 {{ n.votes|commafy }}
+ {% endfor %} +
+ {% else %}

No public notebooks yet — be the first!

{% endif %} +
+ + +
+
+{% endblock %} diff --git a/sites/kaggle/templates/datasets.html b/sites/kaggle/templates/datasets.html new file mode 100644 index 00000000..fea06c1c --- /dev/null +++ b/sites/kaggle/templates/datasets.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} +{% from "_macros.html" import dataset_card %} +{% block title %}Datasets | Kaggle{% endblock %} +{% block content %} +
+

Datasets

+

Explore, analyze, and share quality data.

+ +
+
+ + + + +
+ {{ datasets|length }} datasets +
+ + {% if tag %}

Filtered by tag {{ tag }} clear

{% endif %} + + {% if datasets %} +
{% for d in datasets %}{{ dataset_card(d) }}{% endfor %}
+ {% else %}
No datasets match your search.
{% endif %} + +
+

Popular tags

+
{% for t in all_tags %}{{ t }}{% endfor %}
+
+
+{% endblock %} diff --git a/sites/kaggle/templates/discussion_detail.html b/sites/kaggle/templates/discussion_detail.html new file mode 100644 index 00000000..f223e6aa --- /dev/null +++ b/sites/kaggle/templates/discussion_detail.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_badge %} +{% block title %}{{ d.title }} | Kaggle{% endblock %} +{% block content %} +
+

← {{ d.forum }}

+

{% if d.pinned %}📌 {% endif %}{{ d.title }}

+
+ {% if author %}{{ author.display_name }}{% else %}{{ d.author_username }}{% endif %} + {{ d.created_at|reltime }} + {{ d.forum }} +
+ + + +

{{ comments|length }} Comment{{ 's' if comments|length != 1 }}

+ {% for c in comments %} + + {% endfor %} + + {% if current_user.is_authenticated %} +
+ {{ comment_form.csrf_token }} +
{{ comment_form.body(rows=3, placeholder="Add a comment...") }}
+ +
+ {% else %} +

Sign in to join the discussion.

+ {% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/discussion_new.html b/sites/kaggle/templates/discussion_new.html new file mode 100644 index 00000000..b6b298e9 --- /dev/null +++ b/sites/kaggle/templates/discussion_new.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block title %}New Topic | Kaggle{% endblock %} +{% block content %} +
+

Start a new discussion

+
+ {{ form.csrf_token }} +
{{ form.title(placeholder="What's your topic?") }} + {% for e in form.title.errors %}
{{ e }}
{% endfor %}
+
{{ form.forum() }}
+
{{ form.body(rows=8, placeholder="Share details, questions, or ideas...") }} + {% for e in form.body.errors %}
{{ e }}
{% endfor %}
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/discussions.html b/sites/kaggle/templates/discussions.html new file mode 100644 index 00000000..763ce8f5 --- /dev/null +++ b/sites/kaggle/templates/discussions.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %}Discussions | Kaggle{% endblock %} +{% block content %} +
+
+

Discussions

Ask questions, share ideas, and connect with the community.

+ {% if current_user.is_authenticated %}New Topic{% endif %} +
+ +
+
+ + + + +
+ {{ discussions|length }} topics +
+ + {% if discussions %} +
+ {% for d in discussions %} +
+
+
{% if d.pinned %}📌 {% endif %}{{ d.title }}
+
{{ d.author_username }} · {{ d.forum }} · {{ d.created_at|reltime }}
+
+
👍 {{ d.votes|commafy }}
💬 {{ d.comment_count }}
+
+ {% endfor %} +
+ {% else %}
No discussions match your search.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/index.html b/sites/kaggle/templates/index.html new file mode 100644 index 00000000..e3719bc3 --- /dev/null +++ b/sites/kaggle/templates/index.html @@ -0,0 +1,51 @@ +{% extends "base.html" %} +{% from "_macros.html" import competition_card, dataset_card, notebook_card %} +{% block content %} +
+
+

Level up with the largest data science community

+

Kaggle is where data scientists and machine learners come together to compete, collaborate, learn, and share. Join thousands of competitions, datasets, and notebooks.

+ {% if not current_user.is_authenticated %} + Register with Email + {% else %} + Browse Competitions + {% endif %} +
+
+ +
+
+

Featured Competitions

View all →
+
+ {% for c in featured %}{{ competition_card(c) }}{% endfor %} +
+
+ +
+

Popular Datasets

View all →
+
+ {% for d in hot_datasets %}{{ dataset_card(d) }}{% endfor %} +
+
+ +
+

Trending Notebooks

View all →
+
+ {% for n in trending_notebooks %}{{ notebook_card(n) }}{% endfor %} +
+
+ +
+

Learn Data Science

All courses →
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/leaderboard.html b/sites/kaggle/templates/leaderboard.html new file mode 100644 index 00000000..b807c1cd --- /dev/null +++ b/sites/kaggle/templates/leaderboard.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% block title %}{{ c.title }} — Leaderboard | Kaggle{% endblock %} +{% block content %} +
+

← {{ c.title }}

+

Leaderboard

+

{{ c.title }} · scored by {{ c.metric }}

+ {% if leaderboard %} + + + + {% for s in leaderboard %} + + + + + + + + {% endfor %} + +
#TeamMembersScoreSubmitted
{% if s.rank==1 %}🥇{% elif s.rank==2 %}🥈{% elif s.rank==3 %}🥉{% else %}{{ s.rank }}{% endif %}{{ s.team_name }}{% if user_by_name(s.team_name) %}{{ s.team_name }}{% else %}team{% endif %}{{ '%.5f'|format(s.score) }}{{ s.submitted_at|reltime }}
+ {% else %}
No public submissions yet.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/learn.html b/sites/kaggle/templates/learn.html new file mode 100644 index 00000000..712f3a85 --- /dev/null +++ b/sites/kaggle/templates/learn.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Learn | Kaggle{% endblock %} +{% block content %} +
+

Kaggle Learn

+

Gain the skills you need to do independent data science projects. Free, hands-on, and bite-sized.

+ +
+{% endblock %} diff --git a/sites/kaggle/templates/login.html b/sites/kaggle/templates/login.html new file mode 100644 index 00000000..8bb8dc01 --- /dev/null +++ b/sites/kaggle/templates/login.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block title %}Sign In | Kaggle{% endblock %} +{% block content %} +
+

Sign In

+

Welcome back to Kaggle

+
+ {{ form.csrf_token }} +
{{ form.email(placeholder="you@example.com") }} + {% for e in form.email.errors %}
{{ e }}
{% endfor %}
+
{{ form.password(placeholder="Password") }} + {% for e in form.password.errors %}
{{ e }}
{% endfor %}
+ +
+

New to Kaggle? Register

+
+{% endblock %} diff --git a/sites/kaggle/templates/model_detail.html b/sites/kaggle/templates/model_detail.html new file mode 100644 index 00000000..f4375295 --- /dev/null +++ b/sites/kaggle/templates/model_detail.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ m.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('models', m.thumbnail, m.title, 'detail-thumb') }} +
+

{{ m.title }}

+
+ {{ m.owner_username }} + {{ m.framework }} + {{ m.variations }} variation{{ 's' if m.variations != 1 }} + Updated {{ m.last_updated|reltime }} +
+
+
+ + +
+
+ +
+
+

Model Details

+
{% for para in m.description.split('\n') %}

{{ para }}

{% endfor %}
+ {% if m.tags %}
{% for t in m.tags %}{{ t }}{% endfor %}
{% endif %} +
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/models.html b/sites/kaggle/templates/models.html new file mode 100644 index 00000000..861b77fb --- /dev/null +++ b/sites/kaggle/templates/models.html @@ -0,0 +1,30 @@ +{% extends "base.html" %} +{% from "_macros.html" import model_card %} +{% block title %}Models | Kaggle{% endblock %} +{% block content %} +
+

Models

+

Use and download pre-trained models for your projects.

+ +
+
+ + + + +
+ {{ models|length }} models +
+ + {% if models %} +
{% for m in models %}{{ model_card(m) }}{% endfor %}
+ {% else %}
No models match your search.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/notebook_detail.html b/sites/kaggle/templates/notebook_detail.html new file mode 100644 index 00000000..fd079ce4 --- /dev/null +++ b/sites/kaggle/templates/notebook_detail.html @@ -0,0 +1,60 @@ +{% extends "base.html" %} +{% from "_macros.html" import thumb, tier_badge %} +{% block title %}{{ n.title }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ thumb('notebooks', n.thumbnail, n.title, 'detail-thumb') }} +
+

{{ n.medal_emoji }} {{ n.title }}

+
+ {{ n.author_username }} + {{ n.language }} + Last run {{ n.last_run|reltime }} + {% if n.best_score %}Best score {{ n.best_score }}{% endif %} +
+
+
+ + +
+
+ +
+
+
{% for para in n.description.split('\n') %}

{{ para }}

{% endfor %}
+ {% if n.tags %}
{% for t in n.tags %}{{ t }}{% endfor %}
{% endif %} +
+ +
+
+{% endblock %} diff --git a/sites/kaggle/templates/notebooks.html b/sites/kaggle/templates/notebooks.html new file mode 100644 index 00000000..b1bf20f1 --- /dev/null +++ b/sites/kaggle/templates/notebooks.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% from "_macros.html" import notebook_card %} +{% block title %}Code | Kaggle{% endblock %} +{% block content %} +
+

Code

+

Explore and run machine learning code with Kaggle Notebooks.

+ +
+
+ + + + + +
+ {{ notebooks|length }} notebooks +
+ + {% if notebooks %} +
{% for n in notebooks %}{{ notebook_card(n) }}{% endfor %}
+ {% else %}
No notebooks match your search.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/rankings.html b/sites/kaggle/templates/rankings.html new file mode 100644 index 00000000..b59af8cb --- /dev/null +++ b/sites/kaggle/templates/rankings.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_name, medals %} +{% block title %}Rankings | Kaggle{% endblock %} +{% block content %} +
+

Rankings

+

The Progression System rewards Kagglers for their performance. See who's at the top.

+ +
+ {% for cat in categories %} + {{ cat|capitalize }} + {% endfor %} +
+ + + + + {% for u in users %} + + + + + + + + {% endfor %} + +
#UserTier ({{ category }})MedalsPoints
{{ loop.index }} + + +
{{ u.display_name }} @{{ u.username }}
+
+
{{ tier_name(u.category_tiers.get(category, u.tier)) }}{{ medals(u.gold, u.silver, u.bronze) }}{{ u.points|commafy }}
+
+{% endblock %} diff --git a/sites/kaggle/templates/register.html b/sites/kaggle/templates/register.html new file mode 100644 index 00000000..4e830618 --- /dev/null +++ b/sites/kaggle/templates/register.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block title %}Register | Kaggle{% endblock %} +{% block content %} +
+

Register

+

Join the world's largest data science community

+
+ {{ form.csrf_token }} +
{{ form.email(placeholder="you@example.com") }} + {% for e in form.email.errors %}
{{ e }}
{% endfor %}
+
{{ form.username(placeholder="username") }} + {% for e in form.username.errors %}
{{ e }}
{% endfor %}
+
{{ form.password(placeholder="At least 6 characters") }} + {% for e in form.password.errors %}
{{ e }}
{% endfor %}
+
{{ form.confirm(placeholder="Repeat password") }} + {% for e in form.confirm.errors %}
{{ e }}
{% endfor %}
+ +
+

Already have an account? Sign In

+
+{% endblock %} diff --git a/sites/kaggle/templates/search.html b/sites/kaggle/templates/search.html new file mode 100644 index 00000000..35bf13ff --- /dev/null +++ b/sites/kaggle/templates/search.html @@ -0,0 +1,49 @@ +{% extends "base.html" %} +{% from "_macros.html" import competition_card, dataset_card, notebook_card, model_card, tier_badge %} +{% block title %}Search: {{ q }} | Kaggle{% endblock %} +{% block content %} +
+

Search

+ {% if q %}

{{ total }} result{{ 's' if total != 1 }} for "{{ q }}"

{% else %}

Enter a query to search across Kaggle.

{% endif %} + +
+ {% for s, label in [('all','All'),('competitions','Competitions'),('datasets','Datasets'),('code','Code'),('models','Models'),('discussions','Discussions'),('users','Users')] %} + {{ label }} + {% endfor %} +
+ + {% if comps %} +

Competitions

+
{% for c in comps %}{{ competition_card(c) }}{% endfor %}
+ {% endif %} + {% if datasets %} +

Datasets

+
{% for d in datasets %}{{ dataset_card(d) }}{% endfor %}
+ {% endif %} + {% if notebooks %} +

Code

+
{% for n in notebooks %}{{ notebook_card(n) }}{% endfor %}
+ {% endif %} + {% if models %} +

Models

+
{% for m in models %}{{ model_card(m) }}{% endfor %}
+ {% endif %} + {% if discussions %} +

Discussions

+
{% for d in discussions %} +
+
{{ d.author_username }} · {{ d.forum }}
👍 {{ d.votes }}
+ {% endfor %}
+ {% endif %} + {% if users %} +

Users

+
{% for u in users %} +
+
{{ u.display_name }} {{ tier_badge(u) }}
+
@{{ u.username }} · {{ u.occupation }}
{{ u.points|commafy }} pts
+ {% endfor %}
+ {% endif %} + + {% if q and total == 0 %}
No results found. Try different keywords.
{% endif %} +
+{% endblock %} diff --git a/sites/kaggle/templates/user_profile.html b/sites/kaggle/templates/user_profile.html new file mode 100644 index 00000000..bd41bb92 --- /dev/null +++ b/sites/kaggle/templates/user_profile.html @@ -0,0 +1,56 @@ +{% extends "base.html" %} +{% from "_macros.html" import tier_badge, medals, dataset_card, notebook_card, model_card, competition_card %} +{% block title %}{{ u.display_name }} | Kaggle{% endblock %} +{% block content %} +
+
+ {{ u.username }} +
+

{{ u.display_name }} {{ tier_badge(u) }}

+
@{{ u.username }}{% if u.comp_rank %} · Competitions rank #{{ u.comp_rank }}{% endif %}
+ {% if u.occupation or u.organization %}

{{ u.occupation }}{% if u.organization %} at {{ u.organization }}{% endif %}

{% endif %} + {% if u.location %}

📍 {{ u.location }}

{% endif %} + {% if u.bio %}

{{ u.bio }}

{% endif %} +
{{ medals(u.gold, u.silver, u.bronze) }} · {{ u.points|commafy }} points · {{ follower_count }} followers
+
+
+ {% if current_user.is_authenticated and current_user.username != u.username %} + + {% endif %} +
+
+ +
+ Tiers — {% for cat, t in u.category_tiers.items() %}{{ cat|capitalize }}: {{ t }}{% if not loop.last %} · {% endif %}{% endfor %} +
+ + {% if hosted %} +

Hosted Competitions

+
{% for c in hosted %}{{ competition_card(c) }}{% endfor %}
+ {% endif %} + {% if datasets %} +

Datasets ({{ datasets|length }})

+
{% for d in datasets %}{{ dataset_card(d) }}{% endfor %}
+ {% endif %} + {% if notebooks %} +

Notebooks ({{ notebooks|length }})

+
{% for n in notebooks %}{{ notebook_card(n) }}{% endfor %}
+ {% endif %} + {% if models %} +

Models ({{ models|length }})

+
{% for m in models %}{{ model_card(m) }}{% endfor %}
+ {% endif %} + {% if discussions %} +

Discussions ({{ discussions|length }})

+
{% for d in discussions %} +
+
{{ d.forum }}
👍 {{ d.votes }}
+ {% endfor %}
+ {% endif %} + {% if not (hosted or datasets or notebooks or models or discussions) %} +
{{ u.display_name }} hasn't published any public work yet.
+ {% endif %} +
+{% endblock %} diff --git a/sites/kaggle/verify/verify_0.py b/sites/kaggle/verify/verify_0.py new file mode 100644 index 00000000..cfa4c6cb --- /dev/null +++ b/sites/kaggle/verify/verify_0.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--0 (read-only). + +Find the Getting Started competition about predicting passenger survival on the Titanic +(slug titanic-survival, "Titanic — Machine Learning from Disaster") and report its evaluation +metric. Ground truth: metric == "Classification Accuracy". + +Checks: nav the competition detail page | answer names the metric | LLM anchor. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, scalar, + contains_any, llm_text_match, Judge, parse_args) + +SLUG = "titanic-survival" + +def main(): + a = parse_args() + j = Judge('Kaggle--0', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + gt = scalar(ref, "competitions", "metric", SLUG) # "Classification Accuracy" + j.check("nav_competition", navigated_to(t, f"/competitions/{SLUG}"), "opened the Titanic competition page") + j.check("db_ground_truth", gt is not None, f"metric={gt!r}") + j.check("answer_metric", contains_any(fa, ["classification accuracy", "accuracy"]), f"final={fa!r}") + ok, ev = llm_text_match(fa, f"The evaluation metric is {gt}.", + "What evaluation metric does the Titanic (passenger survival) Getting Started competition use?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_1.py b/sites/kaggle/verify/verify_1.py new file mode 100644 index 00000000..a64e2788 --- /dev/null +++ b/sites/kaggle/verify/verify_1.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--1 (read-only). + +Open 'Home Credit Default Risk 2026' (slug credit-default-risk-2026) and report the scoring +metric. Ground truth: metric == "ROC AUC". + +Checks: nav competition detail | answer names the metric | LLM anchor. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, scalar, + contains_any, llm_text_match, Judge, parse_args) + +SLUG = "credit-default-risk-2026" + +def main(): + a = parse_args() + j = Judge('Kaggle--1', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + gt = scalar(ref, "competitions", "metric", SLUG) # "ROC AUC" + j.check("nav_competition", navigated_to(t, f"/competitions/{SLUG}"), "opened the Home Credit competition page") + j.check("db_ground_truth", gt is not None, f"metric={gt!r}") + j.check("answer_metric", contains_any(fa, ["roc auc", "auc"]), f"final={fa!r}") + ok, ev = llm_text_match(fa, f"The scoring metric is {gt}.", + "Which evaluation metric scores submissions in the Home Credit Default Risk 2026 competition?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_10.py b/sites/kaggle/verify/verify_10.py new file mode 100644 index 00000000..22957509 --- /dev/null +++ b/sites/kaggle/verify/verify_10.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--10 (stateful). + +Sign in as alice.j@test.com and start a new discussion in the 'Questions & Answers' forum +titled 'How do you handle class imbalance?' with a short body. Ground truth (after-state): +a discussions row authored by alice's username (alicejdata) with that title, forum +'Questions & Answers'. No such thread in seed -> no-op FAILs. + +Checks: nav login + new-discussion | DB after: discussion row (author, title, forum). +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, discussion_row, norm, + last_shot, llm_screenshot_shows, Judge, parse_args) + +USERNAME = "alicejdata" +TITLE_SUBSTR = "how do you handle class imbalance" + +def main(): + a = parse_args() + j = Judge('Kaggle--10', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + j.check("nav_login", navigated_to(t, "/login"), f"login={navigated_to(t, '/login')}") + j.check("nav_new_discussion", navigated_to(t, "/discussions/new"), "opened the new-discussion form") + j.check("db_available", after is not None, f"after_db={'ok' if after else None}") + row = discussion_row(after, USERNAME, TITLE_SUBSTR) if after else None # (title, forum) + j.check("db_discussion_created", row is not None, f"row={row!r}") + j.check("db_forum_qa", row is not None and "questions & answers" in norm(row[1]), + f"forum={row[1] if row else None!r}") + s = last_shot(t) + if s: + ok, ev = llm_screenshot_shows(s, "a new discussion titled 'How do you handle class imbalance?' in Questions & Answers", + "the created discussion thread") + j.check("screenshot_shows_thread", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_11.py b/sites/kaggle/verify/verify_11.py new file mode 100644 index 00000000..98451898 --- /dev/null +++ b/sites/kaggle/verify/verify_11.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--11 (stateful). + +Sign in as david.k@test.com, open the discussion 'Underrated Titanic features that actually +help' (slug titanic-feature-ideas), and post a comment thanking the author. Ground truth +(after-state): david's username (davidtran) has MORE comments on that discussion than in the +seed (the seed already has one davidtran comment there, so an existence check is not enough — +we require the count to increase). A no-op agent leaves the count unchanged -> FAIL. + +Checks: nav login + discussion | DB after: davidtran comment count on the thread increased. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, comment_by_on, + last_shot, llm_screenshot_shows, Judge, parse_args) + +USERNAME = "davidtran" +SLUG = "titanic-feature-ideas" + +def main(): + a = parse_args() + j = Judge('Kaggle--11', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + init = resolve_db(a.initial_db, a.container, "instance_seed") + j.check("nav_login", navigated_to(t, "/login"), f"login={navigated_to(t, '/login')}") + j.check("nav_discussion", navigated_to(t, f"/discussions/{SLUG}"), "opened the Titanic features discussion") + before = comment_by_on(init, USERNAME, SLUG) + now = comment_by_on(after, USERNAME, SLUG) + j.check("db_available", before is not None and now is not None, + f"seed={None if before is None else len(before)} after={None if now is None else len(now)}") + j.check("db_comment_added", before is not None and now is not None and len(now) > len(before), + f"seed_count={None if before is None else len(before)} after_count={None if now is None else len(now)}") + s = last_shot(t) + if s: + ok, ev = llm_screenshot_shows(s, "a new comment by David (davidtran) on the Titanic features discussion", + "the discussion comments section") + j.check("screenshot_shows_comment", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_12.py b/sites/kaggle/verify/verify_12.py new file mode 100644 index 00000000..c053711c --- /dev/null +++ b/sites/kaggle/verify/verify_12.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--12 (stateful). + +Sign in as david.k@test.com and update the profile location to 'Boston, United States'. +Ground truth (after-state): david's users.location == 'Boston, United States'. Seed location +is 'Toronto, Canada' -> no-op FAILs. + +Checks: nav login + account edit | DB after: location updated. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, user_location, norm, + last_shot, llm_screenshot_shows, Judge, parse_args) + +EMAIL = "david.k@test.com" + +def main(): + a = parse_args() + j = Judge('Kaggle--12', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + j.check("nav_login", navigated_to(t, "/login"), f"login={navigated_to(t, '/login')}") + j.check("nav_account_edit", navigated_to(t, "/account/edit"), "opened the profile edit form") + j.check("db_available", after is not None, f"after_db={'ok' if after else None}") + loc = user_location(after, EMAIL) if after else None + j.check("db_location_updated", loc is not None and "boston" in norm(loc), f"location={loc!r}") + s = last_shot(t) + if s: + ok, ev = llm_screenshot_shows(s, "profile location set to Boston, United States", + "david's account/profile page") + j.check("screenshot_shows_location", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_13.py b/sites/kaggle/verify/verify_13.py new file mode 100644 index 00000000..0d631d2b --- /dev/null +++ b/sites/kaggle/verify/verify_13.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--13 (read-only). + +Open the Notebooks rankings page. Who is rank #1? Ground truth: the top user in the +'notebooks' ranking (by tier then points) is 'psi_grandmaster' (Priya Sharma). + +Checks: nav rankings | answer names the #1 user | DB anchor (replicates the ranking) | LLM. +""" +import os, sys, json +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, db_query, + contains_any, llm_text_match, Judge, parse_args) + +TIERS = ["Novice", "Contributor", "Expert", "Master", "Grandmaster"] + +def main(): + a = parse_args() + j = Judge('Kaggle--13', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + w = {t_: i for i, t_ in enumerate(TIERS)} + rows = db_query(ref, "SELECT username, display_name, tier, tiers_json, points FROM users WHERE is_org=0") + top_u = top_d = None + if rows: + def key(r): + ct = json.loads(r[3] or "{}").get("notebooks", r[2]) + return (-w.get(ct, 0), -(r[4] or 0)) + best = sorted(rows, key=key)[0] + top_u, top_d = best[0], best[1] + # Require the Notebooks ranking specifically — the default /rankings page is the + # competitions board, so a bare /rankings visit doesn't prove the agent read the + # notebooks tab the task asks about. + j.check("nav_rankings", navigated_to(t, "category=notebooks"), + f"opened the Notebooks rankings tab ({[u for u in [s.get('url','') for s in t.get('steps', [])] if '/rankings' in u]})") + j.check("db_ground_truth", top_u is not None, f"top=({top_u!r},{top_d!r})") + j.check("answer_top_user", top_u is not None and contains_any(fa, [top_u, top_d or top_u]), + f"expected={top_u!r}/{top_d!r} final={fa!r}") + ok, ev = llm_text_match(fa, f"The rank #1 notebooks user is {top_d} ({top_u}).", + "Who is the top-ranked user (rank #1) on the Notebooks rankings page?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_14.py b/sites/kaggle/verify/verify_14.py new file mode 100644 index 00000000..ed5dff92 --- /dev/null +++ b/sites/kaggle/verify/verify_14.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--14 (read-only). + +On Kaggle Learn, how many lessons does 'Intro to Machine Learning' (slug +intro-to-machine-learning) contain? Ground truth: 7 lessons. + +Checks: nav the course page | answer states the lesson count | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, scalar, + contains_number, contains_any, llm_text_match, Judge, parse_args) + +SLUG = "intro-to-machine-learning" +_NUM_WORDS = {1: "one", 2: "two", 3: "three", 4: "four", 5: "five", 6: "six", 7: "seven", + 8: "eight", 9: "nine", 10: "ten", 11: "eleven", 12: "twelve"} + +def main(): + a = parse_args() + j = Judge('Kaggle--14', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + lessons = scalar(ref, "courses", "lessons", SLUG) # 7 + j.check("nav_course", navigated_to(t, f"/learn/{SLUG}"), "opened the Intro to ML course page") + j.check("db_ground_truth", lessons is not None, f"lessons={lessons}") + count_ok = lessons is not None and (contains_number(fa, lessons) + or (lessons in _NUM_WORDS and contains_any(fa, [_NUM_WORDS[lessons]]))) + j.check("answer_lesson_count", count_ok, f"expected={lessons} final={fa!r}") + ok, ev = llm_text_match(fa, f"The course has {lessons} lessons.", + "How many lessons does the 'Intro to Machine Learning' course contain?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_15.py b/sites/kaggle/verify/verify_15.py new file mode 100644 index 00000000..7a68400b --- /dev/null +++ b/sites/kaggle/verify/verify_15.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--15 (read-only chain). + +Open the 'Credit Card Fraud Transactions' dataset, find a public notebook that uses it, open +that notebook, and report its best leaderboard score. Ground truth: the only linked notebook is +'LightGBM Baseline for Real-Time Fraud Detection' (slug lgbm-baseline-fraud), best_score 0.91205. + +Checks: nav dataset + the linked notebook | answer states the best score | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, scalar, db_query, + contains_any, contains_score, llm_text_match, Judge, parse_args) + +DATASET = "credit-card-fraud-transactions" +NOTEBOOK = "lgbm-baseline-fraud" + +def _score_ok(final, best): + """Accept the literal stored score or a rounded form of it (e.g. 0.912).""" + if best is None: + return False + if contains_any(final, [str(best)]): + return True + try: + return contains_score(final, float(best)) + except (TypeError, ValueError): + return False + +def main(): + a = parse_args() + j = Judge('Kaggle--15', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + best = scalar(ref, "notebooks", "best_score", NOTEBOOK) # "0.91205" + j.check("nav_dataset", navigated_to(t, f"/datasets/{DATASET}"), "opened the Credit Card Fraud dataset") + j.check("nav_notebook", navigated_to(t, f"/code/{NOTEBOOK}"), "opened the linked notebook") + j.check("db_ground_truth", best is not None, f"best_score={best!r}") + j.check("answer_best_score", _score_ok(fa, best), f"expected={best!r} final={fa!r}") + ok, ev = llm_text_match(fa, f"The notebook's best leaderboard score is {best}.", + "What is the best leaderboard score of the public notebook that uses the Credit Card Fraud dataset?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_16.py b/sites/kaggle/verify/verify_16.py new file mode 100644 index 00000000..babd98e2 --- /dev/null +++ b/sites/kaggle/verify/verify_16.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--16 (read-only). + +Among Featured competitions that offer a cash prize, which has the largest reward, and how much? +Ground truth: 'Home Credit Default Risk 2026', $100,000 (reward_value 100000). + +Checks: nav competitions | answer names the comp + amount | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, db_query, + contains_any, llm_text_match, Judge, parse_args) + +def main(): + a = parse_args() + j = Judge('Kaggle--16', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + rows = db_query(ref, "SELECT title, reward, reward_value FROM competitions " + "WHERE category='Featured' AND reward_value>0 ORDER BY reward_value DESC") + title = rows[0][0] if rows else None + reward = rows[0][1] if rows else None + j.check("nav_competitions", navigated_to(t, "/competitions"), "opened the competitions listing") + j.check("db_ground_truth", title is not None and (len(rows) < 2 or rows[0][2] > rows[1][2]), + f"top={title!r} reward={reward!r}") + j.check("answer_names_comp", title is not None and contains_any(fa, [title, "home credit"]), + f"expected={title!r} final={fa!r}") + j.check("answer_amount", contains_any(fa, ["100,000", "100000", "$100k"]), f"final={fa!r}") + ok, ev = llm_text_match(fa, f"The largest-cash-prize Featured competition is '{title}' with {reward}.", + "Which Featured cash-prize competition has the largest reward, and how much is it?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_17.py b/sites/kaggle/verify/verify_17.py new file mode 100644 index 00000000..da2cbbba --- /dev/null +++ b/sites/kaggle/verify/verify_17.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--17 (read-only). + +Look at the competitions hosted by 'sara_timeseries'. Which has the earliest deadline? +Ground truth: 'Global Wheat Yield Forecast' (slug global-wheat-yield-forecast, deadline +2026-08-01; next is Arctic Sea Ice 2026-08-20). + +Checks: nav sara's profile | answer names the earliest-deadline comp | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, db_query, + contains_any, llm_text_match, Judge, parse_args) + +HOST = "sara_timeseries" + +def main(): + a = parse_args() + j = Judge('Kaggle--17', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + rows = db_query(ref, "SELECT title, deadline FROM competitions WHERE owner_username=? " + "AND deadline IS NOT NULL ORDER BY deadline", (HOST,)) + title = rows[0][0] if rows else None + j.check("nav_host_profile", navigated_to(t, f"/user/{HOST}"), "opened sara_timeseries's profile") + j.check("db_ground_truth", title is not None and (len(rows) < 2 or rows[0][1] < rows[1][1]), + f"earliest={title!r} rows={[(r[0], r[1]) for r in (rows or [])]}") + j.check("answer_names_comp", title is not None and contains_any(fa, [title, "global wheat yield"]), + f"expected={title!r} final={fa!r}") + ok, ev = llm_text_match(fa, f"The earliest-deadline competition hosted by {HOST} is '{title}'.", + "Which of sara_timeseries's hosted competitions has the earliest deadline?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_18.py b/sites/kaggle/verify/verify_18.py new file mode 100644 index 00000000..b56a55fd --- /dev/null +++ b/sites/kaggle/verify/verify_18.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--18 (read-only). + +Open 'MNIST Handwritten Digits' (slug handwritten-digits-mnist). Under what license? +Ground truth: 'CC0: Public Domain'. + +Checks: nav dataset detail | answer names the license | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, scalar, + contains_any, llm_text_match, Judge, parse_args) + +SLUG = "handwritten-digits-mnist" + +def main(): + a = parse_args() + j = Judge('Kaggle--18', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + lic = scalar(ref, "datasets", "license", SLUG) # "CC0: Public Domain" + j.check("nav_dataset", navigated_to(t, f"/datasets/{SLUG}"), "opened the MNIST dataset page") + j.check("db_ground_truth", lic is not None, f"license={lic!r}") + j.check("answer_license", contains_any(fa, ["cc0", "public domain"]), f"final={fa!r}") + ok, ev = llm_text_match(fa, f"The dataset is released under {lic}.", + "Under what license is the MNIST Handwritten Digits dataset released?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_19.py b/sites/kaggle/verify/verify_19.py new file mode 100644 index 00000000..423776d7 --- /dev/null +++ b/sites/kaggle/verify/verify_19.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--19 (read-only). + +Among Python notebooks that earned a gold medal, which has the most votes, and who is its +author? Ground truth: 'Titanic — Top 3% Solution Walkthrough' by carolwong (2610 votes). + +Checks: nav notebooks (code) | answer names the notebook + author | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, db_query, + contains_any, contains_all, llm_text_match, Judge, parse_args) + +def main(): + a = parse_args() + j = Judge('Kaggle--19', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + rows = db_query(ref, "SELECT title, author_username, votes FROM notebooks " + "WHERE language='Python' AND medal='gold' ORDER BY votes DESC") + title = rows[0][0] if rows else None + author = rows[0][1] if rows else None + j.check("nav_code", navigated_to(t, "/code"), "opened the notebooks (Code) area") + j.check("db_ground_truth", title is not None and (len(rows) < 2 or rows[0][2] > rows[1][2]), + f"top=({title!r},{author!r})") + j.check("answer_names_notebook", title is not None and contains_any(fa, [title, "top 3%"]), + f"expected={title!r} final={fa!r}") + j.check("answer_names_author", author is not None and contains_any(fa, [author]), + f"expected_author={author!r} final={fa!r}") + ok, ev = llm_text_match(fa, f"The most-voted Python gold notebook is '{title}' by {author}.", + "Among Python notebooks with a gold medal, which has the most votes, and who is its author?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_2.py b/sites/kaggle/verify/verify_2.py new file mode 100644 index 00000000..f50db672 --- /dev/null +++ b/sites/kaggle/verify/verify_2.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--2 (read-only). + +Among datasets tagged 'climate', which has the most upvotes? Ground truth: 'Global Temperature +Anomalies 1880–2025' (2240 upvotes; next is CO2 Emissions at 1842). + +Checks: nav the datasets listing | answer names the top dataset | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, db_query, + contains_any, llm_text_match, Judge, parse_args) + +def main(): + a = parse_args() + j = Judge('Kaggle--2', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + rows = db_query(ref, "SELECT title, upvotes FROM datasets WHERE tags_json LIKE '%climate%' ORDER BY upvotes DESC") + top = rows[0][0] if rows else None # "Global Temperature Anomalies 1880–2025" + j.check("nav_datasets", navigated_to(t, "/datasets"), "opened the datasets area") + j.check("db_ground_truth", top is not None and (len(rows) < 2 or rows[0][1] > rows[1][1]), + f"top={top!r} rows={[(r[0], r[1]) for r in (rows or [])][:3]}") + j.check("answer_top_dataset", contains_any(fa, ["global temperature anomalies", "temperature anomalies"]), + f"final={fa!r}") + ok, ev = llm_text_match(fa, f"The most-upvoted climate-tagged dataset is '{top}'.", + "Among datasets tagged 'climate', which one has the most upvotes?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_3.py b/sites/kaggle/verify/verify_3.py new file mode 100644 index 00000000..547ba13d --- /dev/null +++ b/sites/kaggle/verify/verify_3.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--3 (stateful). + +Find the 'Credit Card Fraud Transactions' dataset (slug credit-card-fraud-transactions) and +download it. Ground truth (after-state): the dataset's download counter is incremented vs the +seed (the /datasets//download route bumps it). A no-op agent leaves it unchanged -> FAIL. + +Checks: nav dataset + download route | DB after: downloads(after) > downloads(seed). +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, dataset_downloads, + Judge, parse_args) + +SLUG = "credit-card-fraud-transactions" + +def main(): + a = parse_args() + j = Judge('Kaggle--3', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + init = resolve_db(a.initial_db, a.container, "instance_seed") + j.check("nav_dataset", navigated_to(t, f"/datasets/{SLUG}"), "opened the Credit Card Fraud dataset") + # The download route 302-redirects back to the detail page, so a browser agent may not + # record /download as its own step. The download counter increment (below) is the + # authoritative, fail-closed proof that the download action ran. + before = dataset_downloads(init, SLUG) + now = dataset_downloads(after, SLUG) + j.check("db_available", before is not None and now is not None, f"seed={before} after={now}") + j.check("db_download_incremented", before is not None and now is not None and now > before, + f"downloads seed={before} after={now}") + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_4.py b/sites/kaggle/verify/verify_4.py new file mode 100644 index 00000000..ad206274 --- /dev/null +++ b/sites/kaggle/verify/verify_4.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--4 (read-only). + +On the 'Home Credit Default Risk 2026' leaderboard, which team is #1 and what is their score? +Ground truth: team 'Gradient Surfers', score 0.81342. + +Checks: nav leaderboard/competition | answer names team + score | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, db_query, + contains_any, contains_score, llm_text_match, Judge, parse_args) + +SLUG = "credit-default-risk-2026" + +def main(): + a = parse_args() + j = Judge('Kaggle--4', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + rows = db_query(ref, + "SELECT s.team_name, s.score FROM submissions s JOIN competitions c ON c.id=s.competition_id " + "WHERE c.slug=? ORDER BY s.rank LIMIT 1", (SLUG,)) + team, score = (rows[0][0], rows[0][1]) if rows else (None, None) + j.check("nav_leaderboard", navigated_to(t, f"/competitions/{SLUG}"), "opened the competition/leaderboard") + j.check("db_ground_truth", team is not None, f"top=({team!r},{score})") + j.check("answer_team", team is not None and contains_any(fa, [team]), f"expected_team={team!r} final={fa!r}") + j.check("answer_score", score is not None and contains_score(fa, score), f"expected_score={score} final={fa!r}") + ok, ev = llm_text_match(fa, f"Rank #1 is team '{team}' with score {score}.", + "Which team is ranked #1 on the Home Credit Default Risk 2026 leaderboard, and what is their score?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_5.py b/sites/kaggle/verify/verify_5.py new file mode 100644 index 00000000..0200649b --- /dev/null +++ b/sites/kaggle/verify/verify_5.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--5 (read-only). + +Find a PyTorch model for chest X-ray classification and report its license. +Ground truth: 'ResNet-50 Chest X-Ray Classifier' (slug resnet50-chestxray), license 'Apache 2.0'. + +Checks: nav the model detail page | answer names the license | DB anchor | LLM. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, final_answer, resolve_db, scalar, + contains_any, llm_text_match, Judge, parse_args) + +SLUG = "resnet50-chestxray" + +def main(): + a = parse_args() + j = Judge('Kaggle--5', a.no_llm) + t = load_run(a.run_dir); fa = final_answer(t) + ref = resolve_db(a.initial_db, a.container, "instance_seed") or resolve_db(a.after_db, a.container, "instance") + lic = scalar(ref, "models", "license", SLUG) # "Apache 2.0" + j.check("nav_model", navigated_to(t, f"/models/{SLUG}"), "opened the chest X-ray model page") + j.check("db_ground_truth", lic is not None, f"license={lic!r}") + j.check("answer_license", contains_any(fa, ["apache 2.0", "apache-2.0", "apache"]), f"final={fa!r}") + ok, ev = llm_text_match(fa, f"The model is released under the {lic} license.", + "Under what license is the PyTorch chest X-ray classification model released?") + j.check("answer_llm", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_6.py b/sites/kaggle/verify/verify_6.py new file mode 100644 index 00000000..528566e5 --- /dev/null +++ b/sites/kaggle/verify/verify_6.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--6 (stateful). + +Sign in as alice.j@test.com and join the 'LLM Prompt Recovery' competition +(slug llm-prompt-recovery) with team name 'Data Wizards'. +Ground truth (after-state): a competition_entries row for alice on that competition with +team_name containing 'data wizards'. Alice does NOT pre-join it in the seed -> no-op FAILs. + +Checks: nav login + competition | DB after: entry with team name. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, competition_entry, norm, + last_shot, llm_screenshot_shows, Judge, parse_args) + +EMAIL = "alice.j@test.com" +SLUG = "llm-prompt-recovery" + +def main(): + a = parse_args() + j = Judge('Kaggle--6', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + j.check("nav_login", navigated_to(t, "/login"), f"login={navigated_to(t, '/login')}") + j.check("nav_competition", navigated_to(t, f"/competitions/{SLUG}"), "opened the LLM Prompt Recovery page") + j.check("db_available", after is not None, f"after_db={'ok' if after else None}") + team = competition_entry(after, EMAIL, SLUG) # team_name or None + j.check("db_joined_with_team", team is not None and "data wizards" in norm(team), + f"team={team!r}") + s = last_shot(t) + if s: + ok, ev = llm_screenshot_shows(s, "joined the LLM Prompt Recovery competition as team 'Data Wizards'", + "alice's competition membership") + j.check("screenshot_shows_join", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_7.py b/sites/kaggle/verify/verify_7.py new file mode 100644 index 00000000..d5d819da --- /dev/null +++ b/sites/kaggle/verify/verify_7.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--7 (stateful). + +Sign in as alice.j@test.com and upvote the dataset 'World Happiness Report 2026' +(slug world-happiness-report-2026). Ground truth (after-state): a votes row (alice, 'dataset', +). Alice has no seed vote on it -> no-op FAILs. + +Checks: nav login + dataset | DB after: vote row exists. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, id_by_slug, vote_exists, + last_shot, llm_screenshot_shows, Judge, parse_args) + +EMAIL = "alice.j@test.com" +SLUG = "world-happiness-report-2026" + +def main(): + a = parse_args() + j = Judge('Kaggle--7', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + j.check("nav_login", navigated_to(t, "/login"), f"login={navigated_to(t, '/login')}") + j.check("nav_dataset", navigated_to(t, f"/datasets/{SLUG}"), "opened the World Happiness dataset") + j.check("db_available", after is not None, f"after_db={'ok' if after else None}") + did = id_by_slug(after, "datasets", SLUG) if after else None + voted = vote_exists(after, EMAIL, "dataset", did) if did else None + j.check("db_upvoted", voted is True, f"dataset_id={did} voted={voted}") + s = last_shot(t) + if s: + ok, ev = llm_screenshot_shows(s, "the World Happiness Report 2026 dataset upvoted by the signed-in user", + "the dataset page vote button state") + j.check("screenshot_shows_vote", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_8.py b/sites/kaggle/verify/verify_8.py new file mode 100644 index 00000000..de2a5b0d --- /dev/null +++ b/sites/kaggle/verify/verify_8.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--8 (stateful). + +Sign in as bob.c@test.com and save (bookmark) the notebook 'Titanic — Top 3% Solution +Walkthrough' (slug titanic-top-3-percent). Ground truth (after-state): a bookmarks row +(bob, 'notebook', ). Bob's seed bookmark is a dataset, not this notebook -> no-op FAILs. + +Checks: nav login + notebook | DB after: bookmark row exists. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, id_by_slug, bookmark_exists, + last_shot, llm_screenshot_shows, Judge, parse_args) + +EMAIL = "bob.c@test.com" +SLUG = "titanic-top-3-percent" + +def main(): + a = parse_args() + j = Judge('Kaggle--8', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + j.check("nav_login", navigated_to(t, "/login"), f"login={navigated_to(t, '/login')}") + j.check("nav_notebook", navigated_to(t, f"/code/{SLUG}"), "opened the Titanic Top 3% notebook") + j.check("db_available", after is not None, f"after_db={'ok' if after else None}") + nid = id_by_slug(after, "notebooks", SLUG) if after else None + marked = bookmark_exists(after, EMAIL, "notebook", nid) if nid else None + j.check("db_bookmarked", marked is True, f"notebook_id={nid} bookmarked={marked}") + s = last_shot(t) + if s: + ok, ev = llm_screenshot_shows(s, "the Titanic Top 3% notebook saved/bookmarked by the signed-in user", + "the notebook page save-button state") + j.check("screenshot_shows_bookmark", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_9.py b/sites/kaggle/verify/verify_9.py new file mode 100644 index 00000000..ffeae8c8 --- /dev/null +++ b/sites/kaggle/verify/verify_9.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Verifier for Kaggle--9 (stateful). + +Sign in as bob.c@test.com and follow the user 'psi_grandmaster'. Ground truth (after-state): +a follows row (bob -> 'psi_grandmaster'). Bob has no seed follow -> no-op FAILs. + +Checks: nav login + user profile | DB after: follow row exists. +""" +import os, sys +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from verify_lib import (load_run, navigated_to, resolve_db, follow_exists, + last_shot, llm_screenshot_shows, Judge, parse_args) + +EMAIL = "bob.c@test.com" +TARGET = "psi_grandmaster" + +def main(): + a = parse_args() + j = Judge('Kaggle--9', a.no_llm) + t = load_run(a.run_dir) + after = resolve_db(a.after_db, a.container, "instance") + j.check("nav_login", navigated_to(t, "/login"), f"login={navigated_to(t, '/login')}") + j.check("nav_profile", navigated_to(t, f"/user/{TARGET}"), "opened psi_grandmaster's profile") + j.check("db_available", after is not None, f"after_db={'ok' if after else None}") + fol = follow_exists(after, EMAIL, TARGET) + j.check("db_following", fol is True, f"following={fol}") + s = last_shot(t) + if s: + ok, ev = llm_screenshot_shows(s, "the signed-in user now following psi_grandmaster", + "the profile follow-button state") + j.check("screenshot_shows_follow", ok, ev, llm=True) + j.emit() + +if __name__ == "__main__": + main() diff --git a/sites/kaggle/verify/verify_lib.py b/sites/kaggle/verify/verify_lib.py new file mode 100644 index 00000000..a9a2927d --- /dev/null +++ b/sites/kaggle/verify/verify_lib.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python3 +"""verify_lib.py — shared deterministic + LLM utilities for Kaggle task verification. + +Philosophy: DETERMINISTIC FIRST. + 1. Trajectory navigation check (anti knowledge-shortcut): the agent MUST have + opened the relevant on-site page; a correct answer with no matching navigation + is a memory-recall shortcut = FAIL. + 2. Answer check: exact / regex / token-containment / number match against frozen + ground truth. + 3. DB after-state check (stateful tasks): query the SQLite instance DB directly — + the strongest deterministic signal (competition_entries, votes, bookmarks, + follows, discussions/comments, user profile fields). + 4. LLM utilities (text match, screenshot-contains) are used ONLY where exact + matching is brittle, and are ALWAYS anchored on ground truth: the model + verifies *presence* of given content, never supplies knowledge. One call each. + They SKIP (never fail-close) when the LLM is unavailable, so the deterministic + layer stays authoritative. + +Input signature (per task): + --run_dir DIR agent trajectory dir: trajectory.json + screenshots/step_NNN.png + --initial_db PATH initial-state SQLite DB (default: fetched instance_seed from container) + --after_db PATH after-state SQLite DB (default: fetched live instance DB from container) + --container NAME docker container to fetch DBs from (default: $WH_CONTAINER or wh-review) + --no_llm skip LLM-based checks (run deterministic-only) +Output: JSON {task_id, pass, reason, evidence[]} to stdout; exit 0 on PASS, 1 on FAIL. +""" +import base64, json, os, re, sqlite3, subprocess, sys, tempfile, urllib.request +from pathlib import Path +from dataclasses import dataclass + +SITE = "kaggle" + +# ---------------------------------------------------------------- trajectory +def load_run(run_dir): + d = Path(run_dir) + traj = json.loads((d / "trajectory.json").read_text()) + traj["_run_dir"] = d + traj["_shots"] = {p.name: p for p in sorted((d / "screenshots").glob("step_*.png"))} + return traj + +def step_urls(traj): + return [s.get("url", "") for s in traj.get("steps", [])] + +def navigated_to(traj, substr, times=1): + """Deterministic: at least `times` trajectory steps have a URL containing substr.""" + return sum(1 for u in step_urls(traj) if substr in u) >= times + +def navigated_any(traj, substrs): + return any(navigated_to(traj, s) for s in substrs) + +def final_answer(traj): + return (traj.get("final_answer") or "").strip() + +def _shot(traj, name): + if not name: + return None + p = traj["_shots"].get(Path(name).name) + return p if (p and p.exists()) else None + +def shot_after_url(traj, substr): + for s in traj.get("steps", []): + if substr in s.get("url", ""): + p = _shot(traj, s.get("screenshot_after")) + if p: + return p + return None + +def last_shot(traj): + for s in reversed(traj.get("steps", [])): + p = _shot(traj, s.get("screenshot_after")) or _shot(traj, s.get("screenshot_before")) + if p: + return p + shots = sorted(traj["_shots"].values()) + return shots[-1] if shots else None + +# ---------------------------------------------------------------- deterministic answer match +def norm(s): + return re.sub(r"\s+", " ", (s or "").strip()).casefold() + +def answer_equals(final, expected): + return norm(final) == norm(expected) + +def contains_all(final, tokens): + f = norm(final) + return all(norm(t) in f for t in tokens) + +def contains_any(final, tokens): + f = norm(final) + return any(norm(t) in f for t in tokens) + +def contains_number(final, n): + """True if integer n appears as a standalone number (not a digit inside a larger + number). Avoids '7' matching '17'/'70'/'2027'.""" + return re.search(rf"(? temp file.""" + src = f"{container}:/opt/WebSyn/{SITE}/{kind}/{SITE}.db" + fd, path = tempfile.mkstemp(suffix=".db") + os.close(fd) + r = subprocess.run(["docker", "cp", src, path], capture_output=True, text=True) + if r.returncode != 0: + try: + os.unlink(path) + except OSError: + pass + raise RuntimeError(f"docker cp {src} failed: {r.stderr.strip()}") + return path + +def resolve_db(arg, container, kind): + if arg: + return arg + try: + return fetch_db(container, kind) + except Exception: + return None # caller treats None as "unavailable" and FAILs that check + +def db_query(db_path, sql, params=()): + """Run a query; return rows, or None if the DB can't be opened/queried + (corrupt/locked/missing table) so callers fail-closed instead of crashing.""" + if not db_path: + return None + try: + con = sqlite3.connect(db_path) + try: + return con.execute(sql, params).fetchall() + finally: + con.close() + except sqlite3.Error: + return None + +# --- lookups --- +def user_id_for(db_path, email): + rows = db_query(db_path, "SELECT id FROM users WHERE email=?", (email,)) + return rows[0][0] if rows else None + +def user_location(db_path, email): + rows = db_query(db_path, "SELECT location FROM users WHERE email=?", (email,)) + return rows[0][0] if rows else None + +def id_by_slug(db_path, table, slug): + rows = db_query(db_path, f"SELECT id FROM {table} WHERE slug=?", (slug,)) + return rows[0][0] if rows else None + +# --- stateful after-state helpers (all return None when DB unavailable) --- +def competition_entry(db_path, email, comp_slug): + """(team_name,) for the user's entry in the competition, or None if not joined / DB down.""" + rows = db_query(db_path, + "SELECT ce.team_name FROM competition_entries ce JOIN users u ON u.id=ce.user_id " + "JOIN competitions c ON c.id=ce.competition_id WHERE u.email=? AND c.slug=?", + (email, comp_slug)) + if rows is None: + return None + return rows[0][0] if rows else None + +def vote_exists(db_path, email, entity_type, entity_id): + rows = db_query(db_path, + "SELECT 1 FROM votes v JOIN users u ON u.id=v.user_id " + "WHERE u.email=? AND v.entity_type=? AND v.entity_id=?", (email, entity_type, entity_id)) + return None if rows is None else (len(rows) > 0) + +def bookmark_exists(db_path, email, entity_type, entity_id): + rows = db_query(db_path, + "SELECT 1 FROM bookmarks b JOIN users u ON u.id=b.user_id " + "WHERE u.email=? AND b.entity_type=? AND b.entity_id=?", (email, entity_type, entity_id)) + return None if rows is None else (len(rows) > 0) + +def follow_exists(db_path, email, target_username): + rows = db_query(db_path, + "SELECT 1 FROM follows f JOIN users u ON u.id=f.user_id " + "WHERE u.email=? AND f.target_username=?", (email, target_username)) + return None if rows is None else (len(rows) > 0) + +def discussion_by(db_path, author_username, title_substr): + """True if a discussion authored by author_username has a title containing title_substr.""" + rows = db_query(db_path, + "SELECT 1 FROM discussions WHERE author_username=? AND lower(title) LIKE ?", + (author_username, f"%{title_substr.lower()}%")) + return None if rows is None else (len(rows) > 0) + +def discussion_row(db_path, author_username, title_substr): + rows = db_query(db_path, + "SELECT title, forum FROM discussions WHERE author_username=? AND lower(title) LIKE ?", + (author_username, f"%{title_substr.lower()}%")) + if rows is None: + return None + return rows[0] if rows else None + +def comment_by_on(db_path, author_username, discussion_slug): + """List of comment bodies by author_username on the given discussion, or None.""" + rows = db_query(db_path, + "SELECT cm.body FROM comments cm JOIN discussions d ON d.id=cm.discussion_id " + "WHERE cm.author_username=? AND d.slug=?", (author_username, discussion_slug)) + return None if rows is None else [r[0] for r in rows] + +def dataset_downloads(db_path, slug): + rows = db_query(db_path, "SELECT downloads FROM datasets WHERE slug=?", (slug,)) + return rows[0][0] if rows else None + +def scalar(db_path, table, column, slug): + """Generic single-value fetch by slug (ground-truth anchor).""" + rows = db_query(db_path, f"SELECT {column} FROM {table} WHERE slug=?", (slug,)) + return rows[0][0] if rows else None + +# ---------------------------------------------------------------- shared LLM utilities (anchored) +import simpleArgParser as sap +_NO_LLM = False + + +def _llm_config(): + return (os.environ.get("OPENAI_API_KEY", ""), + os.environ.get("OPENAI_BASE_URL", ""), + os.environ.get("JUDGE_MODEL", "")) + + +def _chat(messages, max_tokens=1024): + if _NO_LLM: + return None + key, base, model = _llm_config() + if not (key and base and model): + return None + payload = {"model": model, "messages": messages, "max_tokens": max_tokens, "temperature": 1.0} + url = base.rstrip("/") + if not url.endswith("/chat/completions"): + url += "/chat/completions" + req = urllib.request.Request(url, data=json.dumps(payload).encode(), + headers={"Content-Type": "application/json", + "Authorization": f"Bearer {key}"}) + try: + data = json.loads(urllib.request.urlopen(req, timeout=180).read()) + except Exception: + return None + try: + return data["choices"][0]["message"]["content"] + except Exception: + return None + +def _verdict(out): + if out is None: + return None, "" + s = out.strip() + if not s: + return None, "" + return s.upper().startswith("PASS"), s + +def llm_text_match(agent_answer, ground_truth, question): + if _NO_LLM: + return None, "[skipped: --no_llm]" + out = _chat([{"role": "user", "content": + f"You are a STRICT binary grader.\nQuestion: {question}\n" + f"Ground-truth answer (ANCHOR — judge against THIS, never use your own knowledge): {ground_truth}\n" + f"Agent's answer: {agent_answer}\n" + f"Decide PASS or FAIL ignoring case/punctuation/word order/surrounding prose. " + f"PASS only if the agent's answer is consistent with the ground truth AND actually answers the question. " + f"Line 1: PASS or FAIL. Line 2: one-sentence reason."}]) + return _verdict(out) + +def llm_screenshot_shows(shot_path, must_show, question=""): + if _NO_LLM: + return None, "[skipped: --no_llm]" + b64 = base64.b64encode(Path(shot_path).read_bytes()).decode() + out = _chat([{"role": "user", "content": [ + {"type": "text", "text": + f"You are a STRICT binary grader. Only what is VISIBLY rendered in this screenshot counts.\n" + f"Question the page should answer: {question}\n" + f"Expected content to verify PRESENCE of: {must_show}\n" + f"PASS only if the expected content (or a semantically equivalent on-screen answer) is visibly shown. " + f"Do NOT use prior knowledge — judge only the rendered pixels.\n" + f"Line 1: PASS or FAIL. Line 2: quote the visible evidence."}, + {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}}]}]) + return _verdict(out) + +# ---------------------------------------------------------------- judge harness + CLI +class Judge: + def __init__(self, task_id, no_llm=False): + global _NO_LLM + _NO_LLM = bool(no_llm) + self.task_id = task_id + self.no_llm = no_llm + self.ok = True + self.reason = "" + self.evidence = [] + + def check(self, name, cond, evidence="", llm=False): + if llm and (self.no_llm or cond is None): + why = "--no_llm" if self.no_llm else "LLM unavailable" + self.evidence.append(f"[SKIP] {name} ({why}): {evidence}") + return True + if cond: + self.evidence.append(f"[PASS] {name}: {evidence}") + else: + self.ok = False + if not self.reason: + self.reason = name + self.evidence.append(f"[FAIL] {name}: {evidence}") + return bool(cond) + + def emit(self): + print(json.dumps({"task_id": self.task_id, "pass": self.ok, + "reason": self.reason, "evidence": self.evidence}, indent=2)) + sys.exit(0 if self.ok else 1) + +def parse_args(): + @dataclass + class VerifyArgs: + run_dir: str = "" + initial_db: str = "" + after_db: str = "" + container: str = os.environ.get("WH_CONTAINER", "wh-review") + no_llm: bool = False + + def post_process(self): + if not self.run_dir: + raise SystemExit("--run_dir is required") + return sap.parse_args(VerifyArgs) diff --git a/websyn_start.sh b/websyn_start.sh index 4d690d78..3355fbac 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -1,11 +1,11 @@ #!/bin/bash -# WebSyn startup: launch all 16 mirror sites, then exec the original CMD. +# WebSyn startup: launch all 17 mirror sites, then exec the original CMD. # This preserves the base image's browser env server (port 8100) as PID 1. set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn merriam_webster) + cambridge_dictionary coursera espn merriam_webster kaggle) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR"