<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Simran Singh</title>
    <description>The latest articles on DEV Community by Simran Singh (@i_am_thor).</description>
    <link>https://dev.to/i_am_thor</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4164213%2Fe72e23f8-4097-4c7b-b188-4d2c3fe7dc26.png</url>
      <title>DEV Community: Simran Singh</title>
      <link>https://dev.to/i_am_thor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/i_am_thor"/>
    <language>en</language>
    <item>
      <title>The Villages That Went Quiet: what 2001 census data can and can't predict in Uttarakhand</title>
      <dc:creator>Simran Singh</dc:creator>
      <pubDate>Sun, 01 Nov 2026 06:51:16 +0000</pubDate>
      <link>https://dev.to/i_am_thor/the-villages-that-went-quiet-what-2001-census-data-can-and-cant-predict-in-uttarakhand-oh0</link>
      <guid>https://dev.to/i_am_thor/the-villages-that-went-quiet-what-2001-census-data-can-and-cant-predict-in-uttarakhand-oh0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hf26"&gt;Hacktoberfest Open-Source AI Challenge&lt;/a&gt; Week 4: Ghost in the Machine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/sssimransingh26-bit/Quiet-Villages" rel="noopener noreferrer"&gt;https://github.com/sssimransingh26-bit/Quiet-Villages&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjwbjhmmjn73evd0ea07m.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjwbjhmmjn73evd0ea07m.jpg" alt="Abandoned Village " width="800" height="993"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing in a ghost village is haunted. That is the unsettling part. There are no cold spots and no footsteps. People did not vanish: they left, one household at a time, for work, for a town with a clinic, for a life the village could not offer. The headlines count what is left behind, for example &lt;a href="https://m.tribuneindia.com/article/migration-empties-734-u-khand-villages/696000" rel="noopener noreferrer"&gt;"Migration empties 734 U'khand villages"&lt;/a&gt;. But if these places are haunted at all, they are haunted by a number: the one the census wrote down in 2001, and the smaller one it wrote down in 2011.&lt;/p&gt;

&lt;p&gt;A census keeps a count the way an ofrenda keeps a name: so that someone, later, can look and say &lt;em&gt;they were here&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I did what a séance would do, with better paperwork. I took the 2001 count and asked it a question, using real data: &lt;strong&gt;looking only at what the 2001 census recorded about a village (how many people lived there, what it had, who they were), can we tell which villages would have lost most of their people by 2011?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What I built is a small, reproducible experiment, not a product:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a table of &lt;strong&gt;13,707 Uttarakhand villages&lt;/strong&gt; (6.2 million people in 2001), each described only by 2001 information;&lt;/li&gt;
&lt;li&gt;a label, fixed before any model was scored: &lt;strong&gt;"depopulated" = 2011 population below 70% of the 2001 population, or no 2011 record at all&lt;/strong&gt;; 1,046 villages (7.6%) qualify;&lt;/li&gt;
&lt;li&gt;a fair test: &lt;strong&gt;whole districts are held out&lt;/strong&gt;, so a model is always scored on districts it has never seen;&lt;/li&gt;
&lt;li&gt;three kinds of predictor compared against each other: a plain rule, standard models, and &lt;strong&gt;TabPFN&lt;/strong&gt;, a pretrained model for small tables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The short version of the result: with all 37 features, &lt;strong&gt;TabPFN scored an AUC of 0.771&lt;/strong&gt; (95% interval 0.756 to 0.786), and 26.7% of the 10% of villages it ranked as most at risk really did lose people, against 7.6% for villages overall. The best standard model I tried, gradient boosting on the same features, scored 0.759 and 25.0%. That is a small edge with overlapping intervals, so I am &lt;strong&gt;not&lt;/strong&gt; claiming TabPFN won. Neither model was tuned. The bigger finding is about the features: who lives in a village (age, work, literacy) told me more than what the village had (roads, clinics, schools).&lt;/p&gt;

&lt;p&gt;There is no live demo. The deliverable is the experiment and the numbers. The tool never lists or ranks individual villages, on purpose, and the section near the end explains why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The quiet, in numbers
&lt;/h2&gt;

&lt;p&gt;The first pattern is the plainest. The smaller the village, the more likely it went quiet:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Village size in 2001&lt;/th&gt;
&lt;th&gt;Villages&lt;/th&gt;
&lt;th&gt;Share that lost ≥30% of people (or vanished)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;50–99&lt;/td&gt;
&lt;td&gt;2,145&lt;/td&gt;
&lt;td&gt;16.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100–249&lt;/td&gt;
&lt;td&gt;4,941&lt;/td&gt;
&lt;td&gt;9.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;250–499&lt;/td&gt;
&lt;td&gt;3,567&lt;/td&gt;
&lt;td&gt;3.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;500–999&lt;/td&gt;
&lt;td&gt;1,884&lt;/td&gt;
&lt;td&gt;2.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000+&lt;/td&gt;
&lt;td&gt;1,170&lt;/td&gt;
&lt;td&gt;5.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(The rise at 1,000+ is something I can't explain from this data. Large villages can be reclassified or merged between censuses, and I did not chase it.)&lt;/p&gt;

&lt;p&gt;Amenities line up with it too. I counted four things a village could have in 2001: a paved road, a primary school, a medical facility and electricity.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Of those four, a village had…&lt;/th&gt;
&lt;th&gt;Villages&lt;/th&gt;
&lt;th&gt;Share depopulated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;976&lt;/td&gt;
&lt;td&gt;15.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;one&lt;/td&gt;
&lt;td&gt;3,856&lt;/td&gt;
&lt;td&gt;10.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;two&lt;/td&gt;
&lt;td&gt;5,189&lt;/td&gt;
&lt;td&gt;6.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;three&lt;/td&gt;
&lt;td&gt;2,568&lt;/td&gt;
&lt;td&gt;4.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;all four&lt;/td&gt;
&lt;td&gt;1,118&lt;/td&gt;
&lt;td&gt;3.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Read this carefully: it is an association, not a cause.&lt;/strong&gt; Small villages are also the ones that lack roads and schools, so I cannot say a road would have kept anyone. The model results below show how much the amenities add &lt;em&gt;after&lt;/em&gt; size is accounted for. It is less than the table makes it look.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Data.&lt;/strong&gt; &lt;a href="https://www.devdatalab.org/shrug" rel="noopener noreferrer"&gt;SHRUG&lt;/a&gt;, the open Socioeconomic High-resolution Rural-Urban Geographic Platform for India from the Development Data Lab, which links the same village across census years. I used its Population Census keys, the 2001 Population Census Abstract and the 2001 Village Directory for Uttarakhand (state code 05). 15,727 villages in 2001; 15,711 can be linked to 2011; 13,707 had at least 50 people in 2001 (below that, a drop from 18 to 12 people is noise, not migration).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A catch in the data.&lt;/strong&gt; The 2011 table has no villages with zero people: the smallest village has 1 person. Villages that emptied completely are simply &lt;em&gt;missing&lt;/em&gt; from it. I labelled units with no 2011 record as depopulated (64 of the 1,046), but some of those may be merges or renames, not exits. I cannot tell them apart, so the 64 are reported, not hidden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Features.&lt;/strong&gt; Two sets, written down before scoring.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;SIZE&lt;/em&gt; (6 features): log population, land area, population density, and the average size and density of the village's sub-district.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;FULL&lt;/em&gt; (37 features): SIZE plus 19 amenities from the Village Directory (schools, medical facilities, tap water, post office, bank, bus, paved and mud roads, distance to the nearest town, power, phone, irrigation, forest share) and 12 census shares (literacy, women's literacy, child share, caste and tribe shares, sex ratio, worker types, household size).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every feature is from 2001. No 2011 number is ever an input.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evaluation.&lt;/strong&gt; Five folds, each holding out whole districts (13 districts in all). I report AUC, average precision, and the hit rate among the 10% of villages each model ranks as most at risk (the base rate is 7.6%), with bootstrap intervals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Models.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Features&lt;/th&gt;
&lt;th&gt;AUC (95% interval)&lt;/th&gt;
&lt;th&gt;Hit rate, riskiest 10% (95% interval)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain rule: "smaller = riskier"&lt;/td&gt;
&lt;td&gt;population only&lt;/td&gt;
&lt;td&gt;0.681 (0.663–0.698)&lt;/td&gt;
&lt;td&gt;17.2% (15.3–19.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logistic regression&lt;/td&gt;
&lt;td&gt;SIZE&lt;/td&gt;
&lt;td&gt;0.659 (0.640–0.677)&lt;/td&gt;
&lt;td&gt;14.8% (13.1–16.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient boosting&lt;/td&gt;
&lt;td&gt;SIZE&lt;/td&gt;
&lt;td&gt;0.702 (0.684–0.718)&lt;/td&gt;
&lt;td&gt;20.6% (18.5–22.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SIZE&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.713&lt;/strong&gt; (0.697–0.729)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;19.1%&lt;/strong&gt; (16.9–21.3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logistic regression&lt;/td&gt;
&lt;td&gt;FULL&lt;/td&gt;
&lt;td&gt;0.714 (0.698–0.730)&lt;/td&gt;
&lt;td&gt;21.5% (19.5–23.7)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient boosting&lt;/td&gt;
&lt;td&gt;FULL&lt;/td&gt;
&lt;td&gt;0.759 (0.743–0.773)&lt;/td&gt;
&lt;td&gt;25.0% (22.8–27.8)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;FULL&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0.771&lt;/strong&gt; (0.756–0.786)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;26.7%&lt;/strong&gt; (24.2–29.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2po198cd23oy1uid6srt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2po198cd23oy1uid6srt.png" alt="Dot-and-interval chart comparing AUC and top-10% hit rate for the plain rule, logistic regression, gradient boosting and TabPFN, using size-only and all-37-feature sets" width="799" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How to read it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;With all 37 features, TabPFN is clearly ahead of the plain rule and of logistic regression. Against gradient boosting the gap is 0.012 AUC and the intervals overlap almost entirely. I did not run a paired test, so I call it a tie in practice.&lt;/li&gt;
&lt;li&gt;With size only, TabPFN (0.713) and gradient boosting (0.702) are also close, and gradient boosting is slightly ahead on hit rate (20.6% vs 19.1%).&lt;/li&gt;
&lt;li&gt;What is clear is the effect of the extra features. For TabPFN, going from 6 features to 37 raised AUC from 0.713 to 0.771 and the hit rate from 19.1% to 26.7%, and those intervals do not overlap.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Which features mattered.&lt;/strong&gt; I asked it two ways. First an ablation with gradient boosting, same folds: village size alone gives an AUC of 0.702. Adding the 19 amenities lifts it to 0.721 (+0.019). Adding the 12 census shares instead lifts it to 0.749 (+0.047). Adding both gives 0.759. Second, permutation importance on the full model: shuffling village size cost the most AUC (0.078), then the share of workers in non-farm, non-household work (0.020), the child share (0.011) and women's literacy (0.009). The only amenity that registered was a primary school (0.005), then a paved road (0.003); power, banks and medical facilities were near zero once size was known. &lt;strong&gt;Size and the shape of the population carry most of the signal; amenities add a little.&lt;/strong&gt; I would not read that as "roads don't matter": a 2001 census table cannot see whether a road was built after 2001, or whether people stayed because of one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TabPFN by the numbers.&lt;/strong&gt; 10 fits (5 district-held-out folds × 2 feature sets), each trained on 10,000 villages with the package's default settings: no tuning, no fine-tuning, no feature engineering for it beyond the 37 columns. It produced 27,414 held-out predictions (13,707 villages × 2 feature sets), and the whole run took 173 seconds. It handled the few missing values in the village directory without any imputation from me. Its results are the two TabPFN rows in the table above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TabPFN.&lt;/strong&gt; TabPFN is a pretrained model that classifies small tables directly, with no hand-tuning. I used it as-is. Its standard row limit is 10,000, so each training fold was down-sampled to 10,000 villages, stratified so the share of depopulated villages stayed the same. The same down-sample was used for both feature sets. The full run (5 folds, 2 feature sets, 10 fits) took about three minutes (173 seconds) on Colab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I can't see from here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I started with a different project.&lt;/strong&gt; It was a scam-call screener. Before fine-tuning anything, I tested whether the untuned model already did the job. It scored 98% on 200 synthetic test calls, so a fine-tune had nothing real to improve, and the remaining errors came from my own synthetic data. I dropped it. The same habit ran through this project: fix the rules first, then look.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No 2021.&lt;/strong&gt; I wanted 2001 to 2021. India's 2021 census was never conducted; the next one is scheduled for 2027 (&lt;a href="https://en.wikipedia.org/wiki/2027_census_of_India" rel="noopener noreferrer"&gt;2027 census of India&lt;/a&gt;). The data ends at 2011.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mid-project change.&lt;/strong&gt; My first scoring used the SIZE features only, because the village directory had not arrived yet. When it arrived I added FULL, with its feature list fixed before I scored it. The label rules never changed. I had seen how many villages fall under the 70% line before choosing it, but not any model's results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only Uttarakhand, only rural villages, only 13 districts.&lt;/strong&gt; Nothing here says the same holds elsewhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intervals are optimistic.&lt;/strong&gt; The bootstrap treats villages as independent, but villages in a district are not. Real uncertainty is wider.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merged units.&lt;/strong&gt; SHRUG's village units are merged where census boundaries changed, so one unit can hide a single village emptying out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prediction is not explanation.&lt;/strong&gt; A model that ranks villages by risk does not say why people left. Work, land, roads and family all matter, and most of that is not in a census table.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why there is not one village name in this post
&lt;/h2&gt;

&lt;p&gt;An ofrenda is made of names. This project has none: only village codes, counts and percentages. I thought about the obvious ending, a map of "the next villages to go quiet", and decided against it, for a reason I can put a number on.&lt;/p&gt;

&lt;p&gt;Among the 10% of villages the best model ranked as most at risk, &lt;strong&gt;26.7% really lost people. That means about three in four did not.&lt;/strong&gt; A ranked list from a model like this would be wrong about most of the villages on it. It would also hang the word "dying" on places where people live. A model can tell you that small villages tended to lose people between 2001 and 2011, and that those without a primary school or a paved road lost them somewhat more often (11.9% against 5.5%, and 8.7% against 4.8%, in the tables above). It cannot tell you which household is deciding tonight whether to go, and it should not be the one to write a village onto a list.&lt;/p&gt;

&lt;p&gt;What the numbers can do is narrower and, I think, more useful: show that the quiet had a shape. It followed size first, then who lived there, and only a little what the village had. People who plan roads, clinics and schools can argue with that shape. I would rather leave them an argument than a hit list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of TabPFN.&lt;/strong&gt; TabPFN is the model under test, scored against the plain rule and standard models on the same held-out districts, with and without the amenity features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;p&gt;Everything here ran on public data. SHRUG is open under CC BY-NC-SA 4.0, and the whole experiment, from raw census files to the final table, can be rerun by anyone. TabPFN's weights are free for non-commercial use but need a license to be accepted first, so I would call it open to use, not open source in the strict sense. That distinction matters when the subject is real communities: anyone can check my numbers, and anyone can see exactly what the model was and was not given.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code and credits
&lt;/h2&gt;

&lt;p&gt;Code, results and the derived table: &lt;a href="https://github.com/sssimransingh26-bit/Quiet-Villages" rel="noopener noreferrer"&gt;https://github.com/sssimransingh26-bit/Quiet-Villages&lt;/a&gt; (code under MIT; the derived data under CC BY-NC-SA 4.0, as SHRUG requires). The README lists the exact steps to reproduce every number in this post.&lt;/p&gt;

&lt;p&gt;Data: Asher, Lunt, Matsuura and Novosad, &lt;em&gt;Development research at high geographic resolution: an analysis of night-lights, firms, and poverty in India using the SHRUG open data platform&lt;/em&gt;, World Bank Economic Review 35(4), 2021. Census data from the Office of the Registrar General &amp;amp; Census Commissioner, India, via SHRUG 2.2.&lt;/p&gt;

</description>
      <category>hacktoberfest</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>Sloth Router: a model that does the least work it safely can, and admits when it shouldn't answer</title>
      <dc:creator>Simran Singh</dc:creator>
      <pubDate>Mon, 26 Oct 2026 06:56:13 +0000</pubDate>
      <link>https://dev.to/i_am_thor/sloth-router-a-model-that-does-the-least-work-it-safely-can-and-admits-when-it-shouldnt-answer-pao</link>
      <guid>https://dev.to/i_am_thor/sloth-router-a-model-that-does-the-least-work-it-safely-can-and-admits-when-it-shouldnt-answer-pao</guid>
      <description>&lt;h2&gt;
  
  
  A scene
&lt;/h2&gt;

&lt;p&gt;A customer of Fern &amp;amp; Thread, a small clothing shop in Pune, writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"how long will shipping take"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One policy, one fact: standard shipping takes 5-7 business days and costs ₹79. A 4B model can answer that for a fraction of a paisa.&lt;/p&gt;

&lt;p&gt;A second customer writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"can I return my sale item and also change my delivery address?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now two policies interact, and sale items are final-sale. That needs a bigger model, or a careful one.&lt;/p&gt;

&lt;p&gt;A third writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"my parcel arrived damaged and I want my money back"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a damage claim and a refund dispute. A human must handle it, and a model guessing here is worse than no answer.&lt;/p&gt;

&lt;p&gt;Sloth Router sorts these three before anyone spends money.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;A small open model (Qwen3.5-4B) sits in front and does one job: pick &lt;strong&gt;ANSWER&lt;/strong&gt; (one policy, clearly covered), &lt;strong&gt;DEFER&lt;/strong&gt; (several policies, or unsure: hand to a bigger model), or &lt;strong&gt;ABSTAIN&lt;/strong&gt; (missing information, or a human must handle it). It is lazy on purpose, for the "Slow and Steady" theme.&lt;/p&gt;

&lt;p&gt;Python, not the model, owns the safety default: a malformed output, a tie, or a low-confidence ANSWER becomes DEFER, never ANSWER.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three rules
&lt;/h2&gt;

&lt;p&gt;The router follows these rules, in order, written into its prompt:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ABSTAIN&lt;/strong&gt; if the request is off-topic, essential information is missing, or a human must handle it (refund disputes, damage claims, chargebacks, personal data). It never collects passwords, cards, Aadhaar or PAN.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DEFER&lt;/strong&gt; if the message asks about two or more policies, needs arithmetic across policies, or the model is unsure. This applies even when each policy is in the knowledge base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ANSWER&lt;/strong&gt; only if the message asks about exactly one policy, a single knowledge-base fact supports the answer, and the request is clear.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If rules overlap, ABSTAIN beats DEFER, and DEFER beats ANSWER. The costly mistake is a confident ANSWER where a DEFER or ABSTAIN was right, so that is the number I treat as the headline: the &lt;strong&gt;dangerous error&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6h9jiepo3unj3g79vbsu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6h9jiepo3unj3g79vbsu.png" alt=" " width="800" height="379"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8zyro6ycar5j91npnej.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq8zyro6ycar5j91npnej.png" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnedu4ws6o7y6cuslffoi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnedu4ws6o7y6cuslffoi.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxozohx8zbfiayfbktxut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxozohx8zbfiayfbktxut.png" alt=" " width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A synthetic help desk for a fictional Pune shop, with a fixed knowledge base.&lt;/li&gt;
&lt;li&gt;An answer-first data generator and a frozen split: 3,000 train, 300 validation, 600 test.&lt;/li&gt;
&lt;li&gt;12 unit tests for the labels and the threshold logic.&lt;/li&gt;
&lt;li&gt;A Tinker LoRA fine-tuning pipeline for Qwen3.5-4B.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/sssimransingh26-bit/Sloth-Router" rel="noopener noreferrer"&gt;https://github.com/sssimransingh26-bit/Sloth-Router&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the data is built
&lt;/h2&gt;

&lt;p&gt;Data is answer-first. Every ANSWER example starts from a knowledge-base sentence, and the question is written and styled afterwards, so the right answer is known before the question exists. DEFER examples combine two policies. ABSTAIN examples drop a needed detail, go off-topic, or demand a human.&lt;/p&gt;

&lt;p&gt;Training uses polite, terse, typo-ridden and Hinglish phrasings. The test set uses email, gen-z, ALL CAPS and staccato styles, plus knowledge-base entries held out from training, so the model can't pass by memorising a style or an entry. Hashes of the frozen files are stored in the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I measured
&lt;/h2&gt;

&lt;p&gt;The untuned 4B router on the 600 test items:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Route accuracy&lt;/td&gt;
&lt;td&gt;64.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dangerous errors (answered when it should have deferred or abstained)&lt;/td&gt;
&lt;td&gt;4.2% (95% CI 2.8% to 6.1%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answer correct&lt;/td&gt;
&lt;td&gt;26.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per 1,000 requests&lt;/td&gt;
&lt;td&gt;$0.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean latency&lt;/td&gt;
&lt;td&gt;3.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These come from an earlier version of my prompt, before I removed a fake confidence number (see below). A 64% untuned baseline leaves real room for fine-tuning to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fine-tuning run
&lt;/h2&gt;

&lt;p&gt;A LoRA fine-tune of Qwen3.5-4B on Tinker. A first smoke test used 64 examples for three epochs: loss fell from 1.70 to 0.25 in the first epoch and reached about 0.02 by the third. That is mostly memorisation of 64 examples, so I treat it as a pipeline check only.&lt;/p&gt;

&lt;p&gt;The real run used 400 examples for one epoch (50 steps), where every example is seen once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Loss&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1.41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.048&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;0.041&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;0.004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0.003&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This shows the model learned the output format quickly. It does not show routing accuracy, because the loss averages over mostly easy format tokens. I have not scored the tuned model on the test set, so I make no claim that it beats the baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;My first training target gave every ANSWER the same confidence of 0.91 and every DEFER 0.74. That number carried no information, so a threshold curve built on it would have been meaningless. I removed it. Real confidence has to come from the model's own probabilities.&lt;/li&gt;
&lt;li&gt;My training examples were first cut off at 728 tokens, which removed exactly the answer I was training on. Training had zero signal until I raised the limit.&lt;/li&gt;
&lt;li&gt;For hours the first Tinker training step would not return. It started working late on deadline day, so I ran out of time to train on the full dataset and score the tuned model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;All data is synthetic, from one fictional shop. There are no real users. Any saving would hold for this help desk only. The project calls Tinker, so it is not fully offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open
&lt;/h2&gt;

&lt;p&gt;A small open-weight model can be fine-tuned, inspected and swapped without changing the rules around it. The safety logic lives in plain Python, not in the model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Score the tuned router against the baseline on the test set, add the always-big and keyword baselines, sweep the threshold on probability-based confidence, and build a small web page.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Grass Owed: screen time becomes outdoor debt</title>
      <dc:creator>Simran Singh</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:56:24 +0000</pubDate>
      <link>https://dev.to/i_am_thor/grass-owed-screen-time-becomes-outdoor-debt-4ecm</link>
      <guid>https://dev.to/i_am_thor/grass-owed-screen-time-becomes-outdoor-debt-4ecm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;My laptop has been quietly charging me interest. Not money. Minutes outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grass Owed&lt;/strong&gt; is a local-first web app that turns screen time into outdoor debt. For every 10 minutes on a screen you owe 2 minutes outside (both numbers are settings). Walking pays it back, and whatever you don't pay carries over to tomorrow. Like a loan, except the lender is a park.&lt;/p&gt;

&lt;p&gt;It reads your calendar (an .ics file; only start and end times are kept, never titles) to find free gaps, and scores each gap with a weather comfort model for your town. Then it nudges you, but only at sensible moments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at most three nudges a day&lt;/li&gt;
&lt;li&gt;quiet from 22:00 to 07:00&lt;/li&gt;
&lt;li&gt;a 90-minute cooldown&lt;/li&gt;
&lt;li&gt;never during an event&lt;/li&gt;
&lt;li&gt;never for tiny debts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also knows when to tell you &lt;em&gt;not&lt;/em&gt; to go. At 2 PM on a June day in Jaipur (39 °C) it says: stay in, go out in the evening. An app that tells you to skip the walk is, I think, an app you can trust.&lt;/p&gt;

&lt;p&gt;I built it for myself: phone at college, laptop in the hostel. It's for anyone who sits at a screen all day and keeps meaning to go outside.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsedjzl34zhx12ahkrkwo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsedjzl34zhx12ahkrkwo.png" alt="Grass Owed app showing 16 minutes of outdoor debt, a best gap of 7:11 to 7:27 pm with comfort 98 out of 100, and a funny nudge about the 31 degree heat" width="632" height="733"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqrpm08x05h8pa0kpgsy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuqrpm08x05h8pa0kpgsy.png" alt="Grass Owed app showing 20 minutes of outdoor debt, 20 minutes on screen and 10 outside, a best gap of 7:12 to 7:32 pm, and a nudge telling me to take a break" width="497" height="731"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4i8adlcpwow5586osix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc4i8adlcpwow5586osix.png" alt="Grass Owed settings screen with walk minutes set to 2 per 5 minutes on screen, funny tone, three nearby green spots, a calendar file upload, and buttons for laptop auto-tracking and notifications" width="533" height="732"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/xpw0j1YM93k" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;In the video: a walkthrough of Grass Owed working, from the debt counter and the best free gap to a nudge appearing and the debt going down.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: I added this video on 12 Oct 2026 after 5 minutes of deadline. The app itself is unchanged.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Run it yourself: clone the repo, open a terminal in the &lt;code&gt;app&lt;/code&gt; folder, run &lt;code&gt;python -m http.server 8000&lt;/code&gt;, and open &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Want to see what it says at a different time of day? Add &lt;code&gt;?t=2026-06-10T14:00&lt;/code&gt; to the address and time-travel to a very hot afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A few real nudges from the app:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Time for a little fresh air. You have 6 minutes owed.&lt;br&gt;
The weather is nice. Try a quick walk now.&lt;br&gt;
The 32 degrees C are perfect for a quick break.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/sssimransingh26-bit" rel="noopener noreferrer"&gt;
        sssimransingh26-bit
      &lt;/a&gt; / &lt;a href="https://github.com/sssimransingh26-bit/Grass-Owed-you-owe-the-park" rel="noopener noreferrer"&gt;
        Grass-Owed-you-owe-the-park
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Grass Owed: You owe the park&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Tracks how much outdoor time you owe, finds the best free gap in your day, and nudges you to go outside with short, funny notifications.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Built for the &lt;strong&gt;Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass&lt;/strong&gt; (DEV)
Project started 7 Oct 2026, inside the challenge window (5-11 Oct 2026). Any commits after the deadline will be listed under "Post-deadline commits" below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Status:&lt;/strong&gt; work in progress.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it is&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debt is the score:&lt;/strong&gt; a daily outdoor-minutes goal and a balance that carries over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gaps come from your calendar file (.ics),&lt;/strong&gt; so suggestions fit your real day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comfort comes from a TabPFN model&lt;/strong&gt; trained on historical weather, so it will not send you out at 2 PM in a hot month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tone is your choice&lt;/strong&gt; (gentle, funny, cheeky). Gemma writes the words; plain code decides when and what.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Architecture&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Build time (Colab, run once):&lt;/strong&gt; TabPFN turns…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/sssimransingh26-bit/Grass-Owed-you-owe-the-park" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The rule I set myself: &lt;strong&gt;the models do their work once, in Colab, and ship two tiny files.&lt;/strong&gt; No model and no server at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  TabPFN: "when is it actually nice outside?"
&lt;/h3&gt;

&lt;p&gt;I pulled 4 years of hourly weather for Jaipur from Open-Meteo (apparent temperature and rain), held out the most recent 12 months, and ran a face-off: a simple baseline (average by month and hour) against TabPFN-3.5 running locally in Colab. TabPFN trained on a 10,000-row sample. The output is a 12 x 24 lookup file, &lt;code&gt;comfort_table.json&lt;/code&gt;, about 13 KB.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;MAE on held-out year (apparent temp, °C)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline (mean by month and hour)&lt;/td&gt;
&lt;td&gt;2.340&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN&lt;/td&gt;
&lt;td&gt;2.303&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;TabPFN wins! By about 0.04 °C, roughly 1.6%. I'll be honest: that's one split scored on a 5,000-row sample, so it could be noise. I'm reporting it as it came out, because a tiny honest win beats a big imaginary one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemma: "say it like a friend, not a calendar"
&lt;/h3&gt;

&lt;p&gt;I ran Gemma 4 E2B (&lt;code&gt;google/gemma-4-E2B-it&lt;/code&gt;, Apache-2.0) once on a free Colab T4 to write the nudge lines: 180 lines across 6 situations and 3 tones (gentle, funny, cheeky), 10 per combination. The app started with 36 hand-written placeholder lines, so this is five times the variety.&lt;/p&gt;

&lt;p&gt;I didn't trust the model blindly. Plain code checks every line for length, allowed placeholders (like &lt;code&gt;{best_time}&lt;/code&gt;), required details and banned words before it's kept. Of 181 lines Gemma returned, 180 passed (99.4%). The one rejected line invented a placeholder that doesn't exist, which is a very creative way to fail.&lt;/p&gt;

&lt;p&gt;Generation took about 20 seconds per call (18 calls, roughly 6 minutes total, about 2 seconds per kept line), and lines average about 60 characters. The result is one small JSON file, so nudges work with no signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  The app
&lt;/h3&gt;

&lt;p&gt;A vanilla JavaScript PWA with no backend. The logic (debt ledger, .ics parser, gap finder, rules engine) lives in one pure module with 8 Node tests. A service worker caches everything so it opens offline.&lt;/p&gt;

&lt;p&gt;On a laptop it counts screen time automatically with the browser's Idle Detection API (Chrome or Edge). On a phone you use start/stop buttons, because browsers can't read phone screen time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How the debt moves
&lt;/h3&gt;

&lt;p&gt;With laptop auto-tracking on, every 10 minutes of active use adds 2 minutes of debt, so it keeps climbing for as long as you work. Walking pays it back one minute for one minute, so a 20-minute walk clears 20 minutes of debt, which is what 100 minutes of screen time creates. Debt never goes below zero and carries over to the next day.&lt;/p&gt;

&lt;h2&gt;
  
  
  I Actually Went Outside
&lt;/h2&gt;

&lt;p&gt;Yes, I tested it outdoors, which feels like the least I could do for a Touch Grass challenge. On 12 October 2026 I went outside for 20 minutes with the app's "I'm out" button running, and my debt went from 40 to 20 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What worked&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The debt number dropped as soon as I tapped "I'm back", and it matched the minutes I was outside.&lt;/li&gt;
&lt;li&gt;Laptop auto-tracking counted my screen minutes without me pressing anything.&lt;/li&gt;
&lt;li&gt;A Gemma-written nudge showed up on the screen.&lt;/li&gt;
&lt;li&gt;It kept working with the network off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What didn't, or isn't done&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The TabPFN improvement is tiny.&lt;/li&gt;
&lt;li&gt;Rain probability per hour is noisy (about 120 days per cell), so comfort scores jump a little between neighbouring hours.&lt;/li&gt;
&lt;li&gt;Calendar time zones are ignored, and repeating events are only expanded daily and weekly for 14 days.&lt;/li&gt;
&lt;li&gt;Notifications only fire while the app is open. Reliable delivery to a closed phone app needs a push server, which I didn't build.&lt;/li&gt;
&lt;li&gt;Laptop auto-tracking works only in Chrome or Edge on desktop.&lt;/li&gt;
&lt;li&gt;It isn't deployed to a public URL, so the demo runs locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because this app knows your calendar and your screen habits, and that is exactly the kind of data you don't want to send to someone else's server.&lt;/p&gt;

&lt;p&gt;Open weights meant both models could run in a free Colab notebook, and their output became two tiny files. There's no cloud API at runtime, no per-request cost, and no personal data leaves the device. Open weather data from Open-Meteo made the training set free to get.&lt;/p&gt;

&lt;p&gt;One honest note: TabPFN's weights are free for non-commercial use under Prior Labs' licence, so this is a hackathon project and not a product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; it predicts outdoor comfort by month and hour from historical weather, exported as the lookup the app uses, with the error reported against a simple baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 E2B wrote the app's 180-line nudge bank, with code validating every line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Credits: weather data from &lt;a href="https://open-meteo.com" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt;. The comfort table was generated with TabPFN-3.5 by Prior Labs GmbH under the TabPFN-3.5 Non-Commercial License. Gemma 4 by Google DeepMind (Apache-2.0).&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
