This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass
On an average Sunday, Pune's naturalists log 12.2 butterfly species. On an average Monday they log 4.0.
I did not believe the butterflies had a schedule, so I built a forecaster, backtested it on a year it had never seen, and then took away the one feature I suspected. The forecast went from useful to useless.
This post is the build, the number, and the afternoon a worker process got kill -9 in the middle of a 13-minute download and the pipeline finished anyway.
What I Built
Fieldcast answers the question that comes before "what is this bird?". It answers which day should I go?
For the next seven days near Pune it forecasts how many butterfly species and how many bird species local observers will log, drops the wet days, and prints the best one on a single page with the species most likely to turn up. You read the card, pocket the phone, and go.
| Place | Pune, 30 km radius (18.52, 73.86) |
| Data | 40,186 research-grade iNaturalist observations (2019 to 4 Oct 2026) and daily Open-Meteo weather |
| Model | TabPFN v2, open weights, CPU only, no training loop |
| Prose | Gemma 3 4B running locally through Ollama |
| Plumbing | Temporal workflow with retries, heartbeats and checkpoints |
| Output | One printable card, light, dark and print layouts |
| Test | 365 held-out days against a seasonal average and gradient boosting |
Demo
Live card: codersacademy006.github.io/fieldcast/card.html
On a phone, which is where you will read it before you leave:
And on paper, so the phone can stay in your pocket:
The forecast it generated on 6 October 2026:
It picks Saturday 10 October: dry, 34.8 C, about 27 butterfly species and 20 bird species expected. The likeliest sightings:
| Species | Model-estimated chance it is logged that day |
|---|---|
| Lemon Migrant | 97% |
| Lemon Pansy | 87% |
| Common Mormon Swallowtail | 77% |
| Red Pierrot | 66% |
Look at that chart again. Five weekdays sit near 19, then the weekend jumps to 27. I will come back to that jump, because it turned out to be the whole story.
Does the forecast work?
I trained on everything before 5 October 2025 (2,469 days) and tested on the next 365 days. Two baselines: a seasonal average (the mean for that time of year) and gradient boosting on the same features.
| Group | Model | Rank correlation (Spearman) | Error (species) | Best-day lift |
|---|---|---|---|---|
| Butterflies | Seasonal average | 0.36 | 6.03 | 0.65 |
| Butterflies | Gradient boosting | 0.48 | 5.34 | 1.45 |
| Butterflies | TabPFN | 0.52 | 5.14 | 1.40 |
| Birds | Seasonal average | -0.04 | 11.40 | 0.74 |
| Birds | Gradient boosting | 0.38 | 9.27 | 1.54 |
| Birds | TabPFN | 0.37 | 8.36 | 1.69 |
Best-day lift: each week I took the day the model ranked highest and divided what was logged that day by the week's average day. 1.0 means the pick was no better than that week's average day. TabPFN's picks averaged 1.4 times the butterfly species and 1.7 times the bird species of a normal day.
TabPFN wins on butterflies and ties gradient boosting on birds, with the lower error. On individual species it beats the seasonal average for 11 of 14 (ROC AUC), and loses on three: Lemon Migrant, Red Pierrot and Rose-ringed Parakeet, which scores 0.476, below a coin flip. AUC measures ranking only; I did not validate calibration, so read the percentages as model estimates, not calibrated probabilities. The full per-species table is in the repo.
That is a decent result. It is also where most weekend projects stop, with a table and a pat on the back. I kept going because of the chart above.
The ablation that ate my result
Why would a model that knows the season and the monsoon predict a weekend jump of 8 species? Because it learned something I handed it. One of my features is a weekend flag.
So I ran the backtest twice, with and without it. Same data, same split, same model.
| Group | Weekend flag | Rank correlation | Best-day lift | Weeks the pick was a weekend day |
|---|---|---|---|---|
| Butterflies | with | 0.52 | 1.40 | 52 of 52 |
| Butterflies | without | 0.33 | 0.95 | 13 of 52 |
| Birds | with | 0.37 | 1.69 | 52 of 52 |
| Birds | without | -0.07 | 1.06 | 21 of 52 |
Without the flag, the butterfly forecast drops to a lift of 0.95, which is slightly worse than picking a day at random. The bird forecast falls to a rank correlation below zero. With the flag, every single week's best day is a weekend day, 52 out of 52.
The forecast is mostly a forecast of people.
Same butterflies, three times the logging
Here are all 2,834 days in the dataset, averaged by weekday. Each number is the count of distinct species logged that day.
| Mon | Tue | Wed | Thu | Fri | Sat | Sun | |
|---|---|---|---|---|---|---|---|
| Butterfly species | 3.98 | 4.18 | 4.29 | 4.37 | 4.40 | 7.82 | 12.22 |
| Bird species | 2.96 | 3.09 | 3.22 | 3.39 | 3.83 | 5.52 | 7.13 |
Weekdays average 4.24 butterfly species and weekends 10.02, a factor of 2.4. For birds it is 3.30 against 6.32, a factor of 1.9. Now the number that explains it: observers log 9.7 observations on a weekday and 25.3 on a weekend day, a factor of 2.6. Divide species by observations and the two kinds of day come out close, about 0.44 species per observation on weekdays and 0.40 on weekends.
Pune does not get more butterflies on Sunday. It gets more people holding phones. The model learned that correctly, and that is the problem with calling it a butterfly forecast.
I kept the flag in the shipped model on purpose. If you are choosing a day for a trip, "a day when a lot will be logged" is a fair proxy for "a day when someone nearby is looking". But the card now says what it is, and I would not put it in front of anyone as a map of where the animals are.
Code
CodersAcademy006
/
fieldcast
TabPFN + local Gemma forecast of which day to go outside near Pune
Fieldcast
Which day this week is best to go outside near Pune, and what will you probably see. TabPFN forecasts daily butterfly and bird species counts from weather and calendar. Local Gemma writes a one-page printable card.
Finding that shaped the project: observers log 2.6 times more observations on weekends, so a forecast of logged species is largely a forecast of people. ablate.py removes the weekend flag and shows how much of the skill disappears.
python3.11 -m venv .venv && .venv/bin/pip install -r requirements.txt
brew install ollama && ollama serve & ollama pull gemma3:4b
.venv/bin/python fetch.py # iNaturalist + Open-Meteo, about 20 min
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py backtest # about 10 min, writes metrics.json
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py forecast # about 9 min, writes forecast.json
.venv/bin/python card.py # writes card.html
Durable run (survives a killed worker, resumes the fetch from its page checkpoint), with the optional Sentry tracing of the Gemma step:
How I Built It
Code owns the numbers, Gemma writes the words, Temporal keeps it all alive.
Features and model
Day of year as sine and cosine, the weekend flag, a slow year trend, and rain and temperature windows over 7 and 30 days. The 30-day rain window is how the model learns the monsoon.
TabPFN is a tabular foundation model, so there is no training loop to tune. One regressor predicts species per day and one classifier per common species predicts "will it be logged today". On a MacBook Air M5 CPU the backtest takes about 10 minutes and the forecast about 6.
Ordinary Python then ranks the seven days, drops the wet ones and builds a data structure that holds every number that will appear on the card.
Temporal: the afternoon I killed the worker
The fetch alone takes about 13 minutes, because iNaturalist pages are slow and polite. A laptop sleeping, a network drop or one bad page should cost a retry, not the whole run. So the pipeline is a Temporal workflow, and each step is an activity:
step = dict(start_to_close_timeout=timedelta(hours=1),
heartbeat_timeout=timedelta(minutes=2),
retry_policy=RetryPolicy(initial_interval=timedelta(seconds=5),
backoff_coefficient=2, maximum_attempts=8))
await asyncio.gather(*[workflow.execute_activity(fetch_obs, g, **step) for g in GROUPS],
workflow.execute_activity(fetch_weather, **step))
await workflow.execute_activity(merge_obs, **step)
await workflow.execute_activity(run_forecast, **step)
return await workflow.execute_activity(make_card, **step)
Butterflies, birds and weather download in parallel. Each long activity runs its blocking code in a thread and heartbeats every 10 seconds, so Temporal can tell a slow step from a dead one. The fetch also writes a checkpoint file after every page.
Then I tested it properly. Mid-fetch, I ran kill -9 on the worker. Temporal got no goodbye. Two minutes later the heartbeat timeout fired, Temporal scheduled attempt 2, I started a fresh worker, and the fetch resumed from the last saved page instead of starting over.
Then it got more interesting, because the run failed for a real reason I had not planned. The forecast step died six times in a row with TabPFN's guard: "Running on CPU with more than 1000 samples is not allowed by default." I had set the override as an environment variable in my shell, and the worker did not have it. I fixed it in code with one line in model.py, restarted the worker, and the pending retry succeeded on attempt 7 without anyone re-running anything.
Total wall time for the whole run, kill, bug and fix included: 24 minutes 42 seconds, status Completed. I checked that the resumed run matches a clean one. The weather table is identical. The resumed data has 40,191 rows against 40,186, and the extra 5 are observations people uploaded in between. The 7-day forecast agrees within 0.4 species on every day.
Gemma: let it talk, never let it count
Gemma 3 4B writes three sentences on the card: why this day, what to look for and in what order, and a reminder to leave the phone alone. It gets the facts in its prompt. Every number on the card comes from code.
A small model told to follow a format does not always follow it, so I measured. Over two batches of 30 cards, 17 and 18 of them broke the three-sentence rule, which is about three in five. Across the 60 cards, zero invented a number. So the checker rejects any draft with the wrong length or any digit that is not in the facts, retries up to five times, and falls back to a plain template if all five fail. The model decides the wording. The code decides what counts as acceptable.
What this does not show
- The backtest feeds the model the actual weather for the test year, not archived forecasts, so real use will score lower.
- Gradient boosting is close behind TabPFN here. I will not claim a big win over it.
- Without the weekend flag, the forecast has little left. That is the finding, and it is also a limit.
- I have not yet carried the card outside and checked it against what I saw.
Run it
CodersAcademy006
/
fieldcast
TabPFN + local Gemma forecast of which day to go outside near Pune
Fieldcast
Which day this week is best to go outside near Pune, and what will you probably see. TabPFN forecasts daily butterfly and bird species counts from weather and calendar. Local Gemma writes a one-page printable card.
Finding that shaped the project: observers log 2.6 times more observations on weekends, so a forecast of logged species is largely a forecast of people. ablate.py removes the weekend flag and shows how much of the skill disappears.
python3.11 -m venv .venv && .venv/bin/pip install -r requirements.txt
brew install ollama && ollama serve & ollama pull gemma3:4b
.venv/bin/python fetch.py # iNaturalist + Open-Meteo, about 20 min
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py backtest # about 10 min, writes metrics.json
TABPFN_ALLOW_CPU_LARGE_DATASET=1 .venv/bin/python model.py forecast # about 9 min, writes forecast.json
.venv/bin/python card.py # writes card.html
Durable run (survives a killed worker, resumes the fetch from its page checkpoint), with the optional Sentry tracing of the Gemma step:
python3.11 -m venv .venv && .venv/bin/pip install -r requirements.txt
brew install ollama && ollama serve & ollama pull gemma3:4b
temporal server start-dev # UI at localhost:8233
.venv/bin/python workflow.py worker # second terminal
.venv/bin/python workflow.py run # third terminal, writes card.html
.venv/bin/python ablate.py # the weekend ablation
Change LAT and LNG in fetch.py and model.py to point it at your own city.
What I Learned
- Run the ablation you are afraid of. One column decided whether this project was a result or an artifact. The tables looked good right up until I removed it.
- A forecast of observations is a forecast of observers. Citizen-science counts carry the habits of the people who log them. Rank correlation will not tell you that. A weekday table will.
-
Durability is a design choice you make before the crash. The heartbeat and the checkpoint cost about ten lines. The
kill -9cost me two minutes of waiting and nothing else. - Make the language model describe and never decide. Gemma is wrong about format three times in five and has never been wrong about a number, because it never makes one.
What's Next
- Replace actual weather with archived forecasts so the backtest matches real use.
- Model observer effort directly, so the forecast separates animals from people.
- Carry the card outside and compare it with what I actually see.
Why Does Open Innovation Matter?
- Anyone can check the weights. TabPFN's v2 checkpoints are public on Hugging Face and the README pins them, so anyone can rerun the backtest and the ablation and get my numbers. The newer v3.5 weights sit behind a licence gate, so I used the open ones and stayed reproducible.
- Almost nothing leaves the machine. The only network calls are two public APIs. Gemma runs locally, so the card writer needs no key and no account, and it sees a weather summary and nothing else.
- The data is a commons. Every row is a person who photographed a butterfly and let strangers identify it. A forecast built on that should be shareable the same way, so the repo has the fetch script, the model, the metrics and the ablation.
Prize Categories
- Best Use of Gemma: Gemma 3 4B runs locally through Ollama and writes the card prose from code-supplied facts, behind a checker that rejects wrong length and invented numbers, measured over 60 runs.
- Best Use of TabPFN: the whole forecast is TabPFN, a regressor for daily counts and a classifier per species, backtested on a held-out year against gradient boosting and a seasonal baseline, then ablated to show how much of its skill is observer behaviour.
- Best Use of Temporal: the pipeline is a Temporal workflow with parallel activities, heartbeats, retries and checkpointed resume, demonstrated by killing the worker mid-fetch and by recovering from a real configuration failure on retry.








Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.