<!-- Generated by scripts/generate_portfolio.py. Edit the HTML pages or now.json, not this file. -->

# Parth Kotwal

> CS @ UW · ML Systems

**Website:** [https://parthkotwal.github.io/](https://parthkotwal.github.io/)

This is the plain-Markdown version of the portfolio, generated from the public website for readers and AI tools.

## About Me

Hello, I'm Parth, a Computer Science student at the University of Washington, Seattle, with a fiery passion for machine learning systems. I’m currently interested in recommendation systems and in developing fail-safe, multi-turn agents through my work at the UW Computer Systems Lab.

### Building right now

- **Eggly** — Social media for tasters

### In my spare time, I enjoy:

- Playing the latest FIFA or badminton
- Following FC Barcelona
- Reviewing restaurants, cafes, and bakeries on [Beli](https://beliapp.co/app/kotwal)
- Expanding my fragrance collection ($n = 14$)

## Experience

- **Software Engineer Intern** — Infoblox • June 2026 - August 2026
- **Machine Learning Researcher** — University of Washington Computer Systems Lab • October 2025 - Present
- **Software Engineer Intern** — miniOrange • July 2024 - September 2024

## Projects

## [Sift](https://parthkotwal.github.io/sift/)

A staged recommendation service over the Yelp Open Dataset. Given a user, it finds businesses they might like from every business in the metro.

**Technologies:** Python · FastAPI · LightGBM · Redis · AWS · Docker

**Source:** [GitHub](https://github.com/parthkotwal/sift)

### Why I built it

I wanted to build a recommender as a complete system rather than stop after training a model in a notebook. Scoring every business with a detailed model takes too long, so Sift became an exercise in deciding what each stage should do, how to evaluate it, and how much time it was allowed to take.

The result is a small API backed by a retrieval, ranking, and reranking pipeline, along with the offline machinery needed to produce its training data and serving features correctly.

### How it works

A request starts with 14,568 businesses in the metro. Exact ALS retrieval keeps 500 candidates, LightGBM reorders all 500 using user and business features, and a final reranking stage removes closed or already-reviewed businesses before applying a category-diversity limit.

**Sift recommendation pipeline:**

- Retrieve — 14.6K → 500 — Exact ALS
- Rank — 500 → 500 — LightGBM
- Rerank — 500 → k — Filters + diversity

The ranking stage deliberately keeps the full pool. An earlier version cut to 50 before filtering, which meant a valid request for 50 recommendations sometimes returned only 33–40. Letting reranking see all 500 means it can return exactly *k* whenever enough eligible candidates exist.

### The part I cared about most

The hardest problem was making the features honest. Every feature on a training row must be computed only from events that happened before that row's timestamp. Sift enforces this through one as-of read path and includes a test that deliberately attempts to leak future information and must fail.

The same feature definitions are materialized in two forms: historical values in Parquet for training and current values in Redis for serving. This keeps the training and serving implementations from quietly drifting apart.

### What surprised me

Filtering businesses that a user had already reviewed lowered recall@10 by roughly 42%. Looking closer made the result more useful, not less: 949 of the ranker's 2,579 top-10 hits were places the user had already reviewed, even though those places were only about 1.3% of the candidate pool.

> More than a third of the apparent success was predicting return visits rather than helping someone discover somewhere new.

I kept the filter and documented the metric regression deliberately. It was a good reminder that a higher offline number is only valuable when it measures the behavior the product actually wants.

### What I learned

Several ideas that looked better on paper did not survive measurement. Exact vector search was already lossless at about 1 ms p99 for this catalog, so HNSW added complexity without a useful speedup. A two-tower model did not beat ALS at its evaluation gate. On AWS, adding a second Uvicorn worker made latency worse; limiting an uncounted OpenBLAS thread pool mattered more.

The deployed service reached a warm server-side p99 of 95.41 ms at 20 requests per second with persistent connections. I tore the AWS deployment down after measuring it on August 1, 2026 to keep the showcase's cost bounded. The repository retains the Docker and Terraform setup, as well as the full measurement context.

## [Olds](https://parthkotwal.github.io/olds/)

A news reader that finds non-obvious connections between stories across topics, then explains why those stories belong together.

**Technologies:** Go · FastAPI · React · sentence-transformers · Supabase · Docker

**Source:** [GitHub](https://github.com/parthkotwal/olds)

### Why I built it

Growing up, I had a habit of reading only the newspaper sections that interested me each morning. Most news feeds make that kind of filtering even easier: once they know what you tend to read, they can keep giving you more of the same.

I built Olds around a different question. Could a personalized feed still help someone wander into another subject by showing the connection to a story they already care about?

### How it works

Olds collects articles from NewsAPI and The Guardian every 30 minutes. A Python service extracts people, places, and organizations from each story and creates a sentence-transformer embedding. The Go backend combines semantic similarity with shared entities to build a directed graph of the articles.

When someone opens a story, the backend traverses that graph for related articles from other topics. It sends the matches to the browser over a WebSocket while an LLM writes a short, plain-English explanation of each connection. The result is less like a conventional “you may also like” row and more like a small piece of context beside the article.

### Personal without ratings

The feed does not ask readers to train it with stars or thumbs. Instead, it learns from quieter signals such as dwell time, scroll depth, and whether an article is opened again.

I liked the tension in that design: the feed should notice what holds a reader's attention without making their world narrower. Cross-topic connections give the recommendation system a way to use familiarity as an entrance to something less obvious.

### What I learned

Olds made recommendation feel less like a ranking problem and more like an explanation problem. Similarity alone can find two nearby articles, but shared entities make the relationship concrete, and a short explanation makes that relationship legible to the reader.

It also pushed me to connect several pieces as one running product: recurring ingestion, Python inference, an in-memory graph in Go, real-time updates, and a React interface backed by Supabase. The project is live at [readolds.fyi](https://readolds.fyi).

## [QueQueue](https://parthkotwal.github.io/quequeue/)

A Spotify queue manager for saving the right sequence of songs and bringing it back when the moment calls for it.

**Technologies:** Vue.js · Django · PostgreSQL · Celery · Redis · Spotify API · Docker

**Source:** [GitHub](https://github.com/parthkotwal/quequeue)

### Why I built it

I kept losing Spotify queues that felt unusually right: the order from a great gym session, a late-night drive, or the stretch before a friend interrupted the music. A playlist preserves a collection, but what I wanted to keep was the particular sequence I had already put together.

QueQueue started as a direct answer to that small annoyance. It lets me save a queue while it is good and restore it later without rebuilding the order by hand.

### What it does

The app connects to Spotify and gives a saved queue a life beyond the current listening session. The interface is built in Vue, while Django handles the API and PostgreSQL stores the account and queue data.

There is a public deployment at [quequeue.app](https://quequeue.app). Spotify currently limits login to users I add to its authorized list, so the [short walkthrough](https://youtu.be/hJ-PSDomcfE) is the easiest way to see the complete save-and-restore flow.

### Restoring a queue

Putting a longer sequence back into Spotify is not a good fit for one web request. QueQueue hands that work to a Celery worker, with Redis coordinating the background job, so the Django API can respond without waiting for every song to be restored.

That separation kept the pieces straightforward: the browser handles the interaction, the API owns the application state, and a worker takes care of the slow operation.

### What I learned

A tiny personal frustration can be a useful reason to build a complete product. QueQueue pushed me past implementing a feature in isolation and into making the frontend, API, database, background worker, Spotify integration, and deployment behave as one application.

It also made the constraints of third-party platforms tangible. The app works because Spotify provides an API, but who can sign in is still shaped by Spotify's authorization rules. Building around that boundary was as much a part of the project as building the queue manager itself.

## [Model United Nations Northwest SMS Admin](https://parthkotwal.github.io/munnorthwest-sms/)

A conference messaging tool for managing participant contacts and sending timely announcements to the right groups.

**Technologies:** FastAPI · Twilio · PostgreSQL · SQLAlchemy · Tailwind CSS

**Source:** [GitHub](https://github.com/parthkotwal/munnorthwest_sms)

### Why I built it

Model United Nations conferences need to communicate with a lot of people at once, but not every announcement belongs in every participant's inbox. Delegates may need one update, advisors another, and staff or secretariat members something more specific.

I built this system for Model United Nations Northwest so conference administrators could keep those contacts organized and send announcements from one practical interface. It is now used by several of the largest Model United Nations conferences in the Pacific Northwest.

### What it does

An administrator selects a conference, imports its participant list from a CSV, and can then search or edit those records in the dashboard. Participants are organized as delegates, advisors, staff, or secretariat, which makes each group available as a messaging audience.

Messages can be sent immediately or scheduled for later. They can also include fields such as a recipient's first name, last name, phone number, or participant type, so a bulk announcement does not have to feel entirely generic.

### Behind each message

The application is built with FastAPI and SQLAlchemy, using PostgreSQL in production and Twilio for delivery. Each conference has its own participants, administrators, colors, and message history, while authentication and CSRF protection keep the operational pages private.

Scheduled messages are checked once a minute. A PostgreSQL advisory lock prevents multiple application workers from processing the same queue at once, and each message keeps a recipient-level delivery record. CSV imports also normalize common encodings and phone-number formats before a contact reaches that queue.

### What I learned

This project made the less glamorous parts of a real administrative tool feel concrete. The interface matters, but so do malformed spreadsheets, duplicate contacts, scheduled jobs, concurrent workers, and a useful record of what was sent.

Building around an actual conference workflow pushed me to think beyond the happy path. The result is not a demo of SMS delivery; it is a small system designed to remain understandable when organizers are using it during a live event.

## [Decido](https://parthkotwal.github.io/decido/)

A multimodal web agent for studying why multi-step browser agents fail—and how much of the answer lies around the model rather than inside it.

**Technologies:** Python · FastAPI · Playwright · gpt-5-nano · Qwen2.5-VL · Modal

**Source:** [GitHub](https://github.com/parthkotwal/decido)

### Where it started

Decido grew from two related ideas. While working as a research assistant at the UW Computer Systems Lab, I was studying whether personal agents fail because they need more training or because they lack context about their own deployment: what they have already done, what comes next, and whether the last action actually worked.

I also wanted to build a web agent after reading AI2's [MolmoWeb paper](https://arxiv.org/abs/2604.08516). Decido borrows its idea of planning with named web skills instead of raw clicks, but takes a different route: it pairs a planner that reads the DOM with one that reads screenshots, then ranks their proposed plans against each other.

### How it works

For each task, a DOM planner and a vision planner propose high-level plans such as filling a form, applying filters, or adding an item to a cart. A ranking layer looks at their confidence, whether the two sources agree, the order of the remaining checklist items, and whether a plan repeats work that has already succeeded.

**Decido browser-agent loop:**

- Propose — DOM + vision — Skill plans
- Ground — Plan → actions — Deterministic compiler
- Execute — Act + verify — Playwright

The winning plan is compiled into primitive browser actions without another model call. Playwright executes and verifies each primitive, and only verified evidence advances the task checklist. An independent evaluator checks any final success claim before the session is allowed to end.

### The experiment I cared about

I ran 34 tasks across six categories three times per configuration. After applying the same shared bug fixes, the older atomic runner completed 70 of 102 sessions. Moving to the skill runner with only the DOM planner completed 93 of 102. Adding the vision planner reached 99 of 102.

> The models stayed the same. Giving them a checklist, ordering context, memory of completed work, and verified execution feedback produced the largest gain.

The comparison is the concrete version of the lab hypothesis: the same underlying intelligence can perform very differently depending on what the surrounding system tells it about the task and its own prior actions.

### Why verification mattered

Browser agents are generous graders of their own work. A page containing the words “Add to cart” does not prove that anything was added, and a confirmation element can exist before a form has been submitted. Most tasks therefore include independent URL or text checks against the final page, and those checks override the agent's self-report in either direction.

Seven of 60 apparent successes from the atomic configuration failed that outside verification. In the skill runner, one of 102 did. The gap made verification part of the result rather than a final testing detail.

### What I learned

Vision helped most when the page itself was visually ambiguous: comparing many prices at once or finding a bare, unlabeled input. It was not universally better, and the DOM-only configuration even won one more run on a multi-page subset. A second planner can add noise as well as signal.

The benchmark also has deliberate limits. It uses live demo sites rather than WebArena-scale tasks, the ranking weights are still hand-tuned, and vision proposes plans but does not ground individual clicks. I see the results as evidence about failure modes and deployment context, not a state-of-the-art claim.

## [Real/Bogus Transient Detection](https://parthkotwal.github.io/braai-cnn/)

A custom CuPy CNN and a PyTorch implementation for separating real astrophysical events from false detections in ZTF image cutouts.

**Technologies:** PyTorch · CuPy · NumPy · scikit-learn · CNNs

**Source:** [GitHub](https://github.com/parthkotwal/braai-cnn)

### The problem

Modern sky surveys produce more candidate events than people can inspect by hand. Some are real transients—objects such as supernovae or comets that change in brightness or position—but many are artifacts from instruments or image processing. The useful signal can be subtle inside a small, noisy cutout.

I used the [braai paper by Duev et al.](https://arxiv.org/abs/1907.11259) as the architectural starting point for studying this real/bogus classification task. My work is inspired by the paper rather than a strict reproduction: the data, split, hyperparameters, and frameworks differ, so the results are not directly comparable.

### What the model sees

The dataset contains 38,368 labeled candidates from the University of Washington's ZTF Alert Archive. Each example contains three 63 × 63 cutouts: the new science image, a reference template, and their difference. About 18,000 examples are labeled real and 20,000 bogus.

That spatial arrangement matters. I compared CNNs that operate directly on the cutouts with Logistic Regression and Random Forest baselines that flatten every example into 11,907 unrelated pixel values.

### Building the CNN twice

I first implemented the VGG6-style network at a low level with NumPy-compatible CuPy operations. Each convolution, pooling, dropout, and fully connected layer owns its forward and backward passes. Converting sliding image patches with `im2col` and accumulating their gradients with `col2im` turned convolution into matrix multiplication and reduced a projected 67-day CPU training run to about 35 minutes on an NVIDIA L4 GPU.

I then rebuilt the same architecture in PyTorch as a benchmark. The framework version trained in under three minutes on a T4 GPU. Writing both made the tradeoff concrete: the custom version exposed initialization, gradient flow, and memory management, while PyTorch made the same model far faster to express and run.

### What the comparison showed

All four models were evaluated on the same held-out set of 9,592 examples. Both CNNs outperformed the flattened baselines, while the custom implementation stayed close to the PyTorch model despite its much longer inference time.

| Model | Accuracy | ROC AUC |
| --- | --- | --- |
| PyTorch CNN | 82.61% | 0.9105 |
| CuPy CNN | 80.17% | 0.8902 |
| Random Forest | 77.50% | 0.8551 |
| Logistic Regression | 66.17% | 0.7113 |

The Random Forest was more competitive than I expected, but the CNN results reinforced why preserving spatial structure is useful for these image triplets.

### What I learned

I began this as a machine-learning exercise and finished with a much better understanding of what sits beneath a framework call. Training stability depended on details such as He and Xavier initialization, early stopping, GPU memory cleanup, and the representation of convolution itself.

The project also changed how I read evaluation results. Accuracy was only one part of the comparison; recall, threshold behavior, inference time, and implementation complexity all affect whether a classifier is practical for a large sky survey. More than anything, implementing the architecture gave me a stronger appreciation for the decisions in the original braai work.

## [Automated Stellar Classification](https://parthkotwal.github.io/star-class-forest/)

An end-to-end exploration of predicting a star's spectral class from its physical properties.

**Technologies:** Python · Scikit-learn · Pandas · NumPy · Matplotlib

**Source:** [GitHub](https://github.com/parthkotwal/Star-Class-Forest)

### What I wanted to explore

I used this project to work through a complete multiclass classification problem with scientific data: understand the observations, prepare them for modeling, compare several approaches, and choose a model using more than one evaluation metric.

The target is a star's spectral class—O, B, A, F, G, K, or M. The model works from measurements such as temperature, luminosity, radius, absolute magnitude, and observed color.

### Preparing the data

The notebooks separate the work into cleaning, preprocessing, exploration, and modeling. I inspected the distribution of spectral classes, normalized inconsistent color labels, and checked the dataset for missing values before turning it into model-ready features.

Temperature, luminosity, and radius are log-transformed, the numeric columns are scaled, and star colors are one-hot encoded. Keeping those steps explicit made it easier to see how the original astronomical measurements became inputs to the classifier.

### Choosing the model

I compared a Random Forest with a support-vector machine, k-nearest neighbors, and a single decision tree. Each model went through randomized hyperparameter search with five-fold cross-validation and was evaluated using accuracy, macro precision, and macro F1.

K-nearest neighbors produced the higher raw accuracy in the notebook's comparison, but the Random Forest offered the better balance of precision and F1. That tradeoff mattered more for a dataset where the classes were not represented evenly, so I used the Random Forest for the final classification report.

### What I learned

The most useful part of the project was seeing how easily a single score can hide the shape of a multiclass problem. Comparing model families was helpful, but choosing between them required deciding what kind of mistakes mattered and reading precision and recall alongside accuracy.

## Contact

- [GitHub](https://github.com/parthkotwal/)
- [LinkedIn](https://www.linkedin.com/in/parthkotwal/)
- [Email](mailto:pkotwal@uw.edu)
