<?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: Malavnaik</title>
    <description>The latest articles on DEV Community by Malavnaik (@malavnaik_0381711024683d4).</description>
    <link>https://dev.to/malavnaik_0381711024683d4</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%2F4185441%2F88b5e936-a67a-4246-bad1-43676c1fc58d.jpg</url>
      <title>DEV Community: Malavnaik</title>
      <link>https://dev.to/malavnaik_0381711024683d4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/malavnaik_0381711024683d4"/>
    <language>en</language>
    <item>
      <title>70% of Ireland's Rent Table Is Empty. Here's How I Cleaned It</title>
      <dc:creator>Malavnaik</dc:creator>
      <pubDate>Mon, 02 Nov 2026 11:42:33 +0000</pubDate>
      <link>https://dev.to/malavnaik_0381711024683d4/70-of-irelands-rent-table-is-empty-heres-how-i-cleaned-it-55ph</link>
      <guid>https://dev.to/malavnaik_0381711024683d4/70-of-irelands-rent-table-is-empty-heres-how-i-cleaned-it-55ph</guid>
      <description>&lt;p&gt;Ireland has some of the best open rent data in Europe. The Residential Tenancies Board (RTB) records the rent of every new tenancy, and the Central Statistics Office (CSO) publishes the averages, every half-year since 2008, for counties, towns and small areas.&lt;/p&gt;

&lt;p&gt;I wanted to use it for a quick analysis and found the raw table hard to work with. So I cleaned it properly and published the result as an open dataset on &lt;a href="https://www.kaggle.com/datasets/malavnaik/irish-rents-rtb" rel="noopener noreferrer"&gt;Kaggle&lt;/a&gt; and &lt;a href="https://huggingface.co/datasets/Malavnaik/irish-rents-rtb" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's wrong with the raw table
&lt;/h2&gt;

&lt;p&gt;The CSO table (RIH02) has 674,352 rows. Three things make it awkward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every category appears twice&lt;/strong&gt;, once as a code and once as a label, plus columns that never change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Places of very different sizes are mixed together&lt;/strong&gt;: counties, towns, Dublin postal districts and small areas sit in one column with nothing saying which is which.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;70% of the rent cells are empty.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Empty doesn't mean zero
&lt;/h2&gt;

&lt;p&gt;The empty cells are the most important part. The RTB leaves a cell empty when too few new tenancies were registered to give a reliable average. It's mostly small areas and specific bedroom counts: 86% of "one bed" cells are empty, against 50% for "all bedrooms".&lt;/p&gt;

&lt;p&gt;If you load the file and fill those gaps with zeros, or let a tool do it quietly, every average you compute is wrong. So I dropped them and wrote down why in the data card. 201,912 published rents remain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Labelling places: my first try was wrong
&lt;/h2&gt;

&lt;p&gt;I first guessed each place's level from the CSO's location codes, assuming counties ended in four zeros. The check I printed found only &lt;strong&gt;4 "counties"&lt;/strong&gt;, and one of them was Christchurch, an area of Dublin city. So the guess was wrong, and the printout caught it before anything was published.&lt;/p&gt;

&lt;p&gt;I switched to matching the 26 county names. Everything without a comma ("Carlow Town", "Dublin 8") is a town or district, and everything with one ("Tullow, Carlow") is a smaller area: 26 counties, 36 towns and districts, 375 areas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Totals, flagged
&lt;/h2&gt;

&lt;p&gt;"All bedrooms", "1 to 2 bed" and "All property types" are sums of other rows. Leaving them unmarked invites double counting, so each row has two true/false columns that say whether it's a total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checks, then publish
&lt;/h2&gt;

&lt;p&gt;Before anything goes out, the pipeline asserts one row per half-year, place, bedrooms and property type, and that every rent is between €50 and €20,000. A GitHub Actions workflow downloads the official table, cleans it, publishes the files with a data card, and runs the same code as a &lt;a href="https://www.kaggle.com/code/malavnaik/cleaning-irish-rents-rtb" rel="noopener noreferrer"&gt;Kaggle notebook&lt;/a&gt;. Both produce the same 201,912 rows. The &lt;a href="https://github.com/malavnaik99/irish-rents-dataset" rel="noopener noreferrer"&gt;code is on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What 18 years of rents show
&lt;/h2&gt;

&lt;p&gt;Average monthly rent for a new tenancy, all homes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dublin&lt;/strong&gt; fell from €1,325 in early 2008 to &lt;strong&gt;€1,004 in early 2011&lt;/strong&gt;, then more than doubled to &lt;strong&gt;€2,162&lt;/strong&gt; by late 2025.&lt;/li&gt;
&lt;li&gt;Over the last ten years, rents &lt;strong&gt;at least doubled in 17 of 26 counties&lt;/strong&gt;. The biggest rises were in &lt;strong&gt;Longford (+156%)&lt;/strong&gt;, &lt;strong&gt;Limerick (+146%)&lt;/strong&gt; and &lt;strong&gt;Roscommon (+133%)&lt;/strong&gt;. Dublin rose 67%, from a much higher base.&lt;/li&gt;
&lt;li&gt;Late 2025, the most expensive counties were Dublin (€2,162), Wicklow (€1,739) and Kildare (€1,719). The least expensive were Donegal (€992), Leitrim (€1,010) and Monaghan (€1,034).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are averages for &lt;strong&gt;new&lt;/strong&gt; tenancies, not what all tenants pay, and they aren't adjusted for inflation. The chart is on the &lt;a href="https://malavnaik.com/projects/irish-rents/" rel="noopener noreferrer"&gt;project page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adjust for inflation&lt;/strong&gt; using the CSO's consumer price index, so 2008 and 2025 rents can be compared fairly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add tenancy counts&lt;/strong&gt;, if the RTB publishes them, so users can see how reliable each average is.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dataset is free to use under CC BY 4.0. If you build something with it, I'd love to see it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Featured product: &lt;a href="https://play.google.com/store/apps/details?id=com.cadentiq.app&amp;amp;referrer=utm_source%3Dmalavnaik%26utm_medium%3Ddevto%26utm_campaign%3Dfeatured" rel="noopener noreferrer"&gt;Cadentiq&lt;/a&gt; · habits, walks, workouts and focus in one Android app.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opendata</category>
      <category>python</category>
      <category>datacleaning</category>
      <category>ireland</category>
    </item>
    <item>
      <title>I Built a Search Tool for the EU Digital Services Act, and Tested It First</title>
      <dc:creator>Malavnaik</dc:creator>
      <pubDate>Mon, 02 Nov 2026 11:42:13 +0000</pubDate>
      <link>https://dev.to/malavnaik_0381711024683d4/i-built-a-search-tool-for-the-eu-digital-services-act-and-tested-it-first-3pn6</link>
      <guid>https://dev.to/malavnaik_0381711024683d4/i-built-a-search-tool-for-the-eu-digital-services-act-and-tested-it-first-3pn6</guid>
      <description>&lt;p&gt;The EU's Digital Services Act (DSA) sets the rules for how online platforms handle illegal content, appeals, ads and transparency. If you work in trust &amp;amp; safety, you keep needing to find &lt;em&gt;the&lt;/em&gt; article that covers a situation. But you think in everyday words ("can they remove my post without saying why?"), and the law uses its own ("statement of reasons").&lt;/p&gt;

&lt;p&gt;So I built a small search tool: type a question, get the three articles most likely to answer it, each with the matching paragraph quoted and a link to the official text. You can &lt;a href="https://malavnaik.com/projects/ask-the-dsa/" rel="noopener noreferrer"&gt;try it on the project page&lt;/a&gt;. It runs in your browser, so nothing you type is sent anywhere.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not legal advice. The tool only quotes the law.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it quotes the law instead of answering
&lt;/h2&gt;

&lt;p&gt;The popular approach is "RAG": find the relevant text, then have an AI model write an answer from it. For law, I chose to stop at the first half. A generated answer can sound confident and still get the law wrong; a quoted paragraph with a link can always be checked. It also meant the whole thing could run for free, with no model server to pay for.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Split the law into searchable pieces
&lt;/h2&gt;

&lt;p&gt;I took the official English text from EUR-Lex (the EU's law website, whose documents may be reused with credit) and split it into one chunk per paragraph of each article. Long lists, like the definitions in Article 3, are split into their points. Each chunk keeps its article number, title, chapter and link. That gave 638 chunks (93 articles plus 156 recitals), which I published as an &lt;a href="https://huggingface.co/datasets/Malavnaik/eu-dsa-articles" rel="noopener noreferrer"&gt;open dataset on Hugging Face&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Write the test before choosing the method
&lt;/h2&gt;

&lt;p&gt;Before building the demo, I wrote &lt;strong&gt;25 questions in everyday language&lt;/strong&gt;, each with the article(s) that answer it, checked against the text. Then I compared three ways of searching:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keyword search (BM25):&lt;/strong&gt; the classic approach, matching words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic search:&lt;/strong&gt; a small open model (&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;) turns text into vectors, so it can match meaning, not just exact words.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid:&lt;/strong&gt; both rankings combined.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Keyword&lt;/th&gt;
&lt;th&gt;Semantic&lt;/th&gt;
&lt;th&gt;Hybrid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Right article ranked first&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;td&gt;52%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right article in the top 3&lt;/td&gt;
&lt;td&gt;68%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;84%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right article in the top 5&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The demo shows three articles, so I shipped semantic search: it had the right article in the top 3 most often. The full evaluation is in a &lt;a href="https://www.kaggle.com/code/malavnaik/searching-the-eu-digital-services-act" rel="noopener noreferrer"&gt;Kaggle notebook&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Run it in the browser
&lt;/h2&gt;

&lt;p&gt;The demo uses a compressed copy of the same model (about 23 MB), downloaded once by the visitor's browser, plus the pre-computed vectors for every chunk. I re-ran the 25 questions with the compressed model, and it is weaker: the right article was in the top 3 for &lt;strong&gt;72%&lt;/strong&gt; of questions (56% first, 92% in the top 5). That's the honest cost of a free, private demo, and why it always shows three results with links rather than one "answer".&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fails
&lt;/h2&gt;

&lt;p&gt;The misses are telling. "Does a platform have to explain why it removed my post?" should find Article 17, but the law calls this a &lt;em&gt;statement of reasons&lt;/em&gt; and never uses the word "explain". The everyday question and the legal wording barely overlap, and a small model doesn't always bridge the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write more test questions&lt;/strong&gt;, ideally with people who work with the DSA, because 25 questions is a small test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try a stronger embedding model&lt;/strong&gt; and see whether the gains survive compression for the browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;€0: a free static Hugging Face Space, a free dataset, and GitHub Actions and Kaggle on their free tiers. The &lt;a href="https://github.com/malavnaik99/eu-dsa-search" rel="noopener noreferrer"&gt;code is on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source of the law: EUR-Lex, © European Union. Only the Official Journal text is authentic.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Featured product: &lt;a href="https://play.google.com/store/apps/details?id=com.cadentiq.app&amp;amp;referrer=utm_source%3Dmalavnaik%26utm_medium%3Ddevto%26utm_campaign%3Dfeatured" rel="noopener noreferrer"&gt;Cadentiq&lt;/a&gt; · habits, walks, workouts and focus in one Android app.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>trustandsafety</category>
      <category>rag</category>
      <category>nlp</category>
      <category>eu</category>
    </item>
    <item>
      <title>Why '2 Hours of Maintenance' Becomes 3 Days: The Engineering Behind Failed Deployments and Rollbacks</title>
      <dc:creator>Malavnaik</dc:creator>
      <pubDate>Tue, 27 Oct 2026 14:53:15 +0000</pubDate>
      <link>https://dev.to/malavnaik_0381711024683d4/why-2-hours-of-maintenance-becomes-3-days-the-engineering-behind-failed-deployments-and-rollbacks-1pf5</link>
      <guid>https://dev.to/malavnaik_0381711024683d4/why-2-hours-of-maintenance-becomes-3-days-the-engineering-behind-failed-deployments-and-rollbacks-1pf5</guid>
      <description>&lt;p&gt;(&lt;a href="https://malavnaik.com/blog/maintenance-window-failed-deployment-rollback/" rel="noopener noreferrer"&gt;https://malavnaik.com/blog/maintenance-window-failed-deployment-rollback/&lt;/a&gt;)&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Built a Tested Data Pipeline for Dublin Bikes, for €0</title>
      <dc:creator>Malavnaik</dc:creator>
      <pubDate>Mon, 26 Oct 2026 18:48:31 +0000</pubDate>
      <link>https://dev.to/malavnaik_0381711024683d4/i-built-a-tested-data-pipeline-for-dublin-bikes-for-eu0-3kc8</link>
      <guid>https://dev.to/malavnaik_0381711024683d4/i-built-a-tested-data-pipeline-for-dublin-bikes-for-eu0-3kc8</guid>
      <description>&lt;p&gt;The Dublinbikes feed tells you how many bikes are at each station &lt;em&gt;right now&lt;/em&gt;. It doesn't keep history. So if you want to ask "how do weather and time of day change how many bikes are free?", you first need a pipeline that collects the data, keeps it safe and tells you when something breaks.&lt;/p&gt;

&lt;p&gt;I built one, end to end, using only free tiers. The code is &lt;a href="https://github.com/malavnaik99/dublin-data-pipeline" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; and the &lt;a href="https://malavnaik.com/projects/dublin-bikes-pipeline/" rel="noopener noreferrer"&gt;project page&lt;/a&gt; has a live dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Collect: small, boring and append-only
&lt;/h2&gt;

&lt;p&gt;A Python script runs on GitHub Actions every 15 minutes. It reads the Dublinbikes GBFS feed (all 115 stations) and, once an hour, the Met Éireann observations for Dublin Airport. Both are open data under CC BY 4.0.&lt;/p&gt;

&lt;p&gt;Each run writes a new Parquet file, partitioned by date, to a public Hugging Face dataset. Nothing is ever overwritten, so a bad run can't damage history.&lt;/p&gt;

&lt;p&gt;Two lessons from this step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub's own scheduler is unreliable for frequent jobs.&lt;/strong&gt; My 15-minute schedule didn't fire once in its first hour. A free external timer (cron-job.org) that calls GitHub's API fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail loudly, then stop.&lt;/strong&gt; After three failed runs in a row the workflow switches itself off and sends me a Telegram message, instead of failing silently for days.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Model: dbt on DuckDB
&lt;/h2&gt;

&lt;p&gt;Every morning, dbt builds the models inside a DuckDB file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Staging:&lt;/strong&gt; proper types, UTC timestamps (the weather comes in Irish local time) and de-duplication. Each hourly weather file repeats the whole day so far, so only the latest copy of each observation is kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intermediate:&lt;/strong&gt; one row per station per hour, with average, minimum and maximum bikes, and the share of the hour the station was empty or full.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marts:&lt;/strong&gt; a small star schema. &lt;code&gt;fct_station_hour&lt;/code&gt; sits in the middle, with &lt;code&gt;dim_station&lt;/code&gt; (name, location, capacity) and &lt;code&gt;dim_hour&lt;/code&gt; (Dublin local time, weekday and that hour's weather).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fact table is &lt;strong&gt;incremental&lt;/strong&gt;: each run only recomputes the last three hours. It is also &lt;strong&gt;idempotent&lt;/strong&gt;: running it twice gives exactly the same table. That matters, because reruns are the first thing you reach for when something goes wrong.&lt;/p&gt;

&lt;p&gt;DuckDB writes everything to a single file, so I store it in a second Hugging Face dataset between runs. That's how the incremental table "remembers" what it already built without a database server.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test: 14 checks, every run
&lt;/h2&gt;

&lt;p&gt;dbt runs 14 data tests on every build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keys are unique and never empty;&lt;/li&gt;
&lt;li&gt;every row in the fact table links to a station and an hour;&lt;/li&gt;
&lt;li&gt;bike and dock counts are never negative;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;freshness&lt;/strong&gt;: the newest snapshot is less than 2 hours old;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;volume&lt;/strong&gt;: at least 75% of the expected snapshots arrived in the last 24 hours.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two catch the real-world failures: a feed that goes quiet, or a collector that silently stops. If any check fails, I get a Telegram alert.&lt;/p&gt;

&lt;p&gt;The data also taught me something: one "station" in the feed is a placeholder that never has a bike. Left in, it tops every "most often empty" list. The dashboard now leaves out stations that never had a bike in the period.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Publish: a dashboard with no backend
&lt;/h2&gt;

&lt;p&gt;Each run publishes the tables as Parquet, a run log and a small &lt;code&gt;dashboard.json&lt;/code&gt; summary. The &lt;a href="https://malavnaik.com/projects/dublin-bikes-pipeline/" rel="noopener noreferrer"&gt;project page&lt;/a&gt; fetches that file in the browser and draws the chart. There's no server to keep running, and the dashboard updates as soon as the morning run finishes. The &lt;a href="https://huggingface.co/spaces/Malavnaik/dublin-bikes-dbt-docs" rel="noopener noreferrer"&gt;dbt docs and lineage graph&lt;/a&gt; are published as a free static page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;€0: public-repo GitHub Actions minutes, free Hugging Face datasets and a static Space, and one free cron-job.org job.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compact the small files.&lt;/strong&gt; About 100 small Parquet files a day is fine for now, but over a year I'd merge them into daily files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a "late data" test per station&lt;/strong&gt;, not just overall, to spot a single broken dock.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The pipeline is the easy part. The question needs weeks of data across different weather before any answer is honest. Once that has built up, I'll publish the analysis and write up what the data says.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>dbt</category>
      <category>duckdb</category>
      <category>python</category>
    </item>
    <item>
      <title>What 1.8 Million Comments Taught Me About Fair Content Moderation</title>
      <dc:creator>Malavnaik</dc:creator>
      <pubDate>Mon, 26 Oct 2026 18:46:25 +0000</pubDate>
      <link>https://dev.to/malavnaik_0381711024683d4/what-18-million-comments-taught-me-about-fair-content-moderation-2828</link>
      <guid>https://dev.to/malavnaik_0381711024683d4/what-18-million-comments-taught-me-about-fair-content-moderation-2828</guid>
      <description>&lt;p&gt;&lt;em&gt;Content warning: this project uses a dataset of real toxic comments. This post doesn't quote any of them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You can't share real moderation data from work, and you shouldn't. But you can practise every core trust and safety analytics skill on public data. So I did: I trained two content moderation models on 1.8 million public comments and measured them the way a trust and safety team would.&lt;/p&gt;

&lt;p&gt;Everything is public: the &lt;a href="https://www.kaggle.com/code/malavnaik/moderation-classifier-civil-comments" rel="noopener noreferrer"&gt;Kaggle notebook&lt;/a&gt;, the &lt;a href="https://huggingface.co/Malavnaik/civil-comments-distilbert-moderation" rel="noopener noreferrer"&gt;model and its model card&lt;/a&gt;, a &lt;a href="https://huggingface.co/spaces/Malavnaik/moderation-classifier-demo" rel="noopener noreferrer"&gt;demo that runs in your browser&lt;/a&gt; and the &lt;a href="https://github.com/malavnaik99/moderation-classifier" rel="noopener noreferrer"&gt;code&lt;/a&gt;. The project write-up is &lt;a href="https://malavnaik.com/projects/moderation-classifier/" rel="noopener noreferrer"&gt;on my site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.kaggle.com/competitions/jigsaw-unintended-bias-in-toxicity-classification" rel="noopener noreferrer"&gt;Civil Comments&lt;/a&gt; dataset has 1.8 million comments from news sites, each rated by several people for toxicity, insults, threats and more. It's released under CC0, so anyone can use it. Crucially, a subset is also labelled with the identity groups each comment &lt;em&gt;mentions&lt;/em&gt;, which makes a fairness check possible. 8% of comments are toxic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two models
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A simple baseline:&lt;/strong&gt; TF-IDF word features with logistic regression. Fast, cheap and surprisingly strong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A modern model:&lt;/strong&gt; DistilBERT, a small transformer that reads words in context, fine-tuned on Kaggle's free GPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every number below comes from 180,488 test comments that neither model saw during training.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Simple baseline&lt;/th&gt;
&lt;th&gt;DistilBERT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ranking quality (ROC-AUC, 1 = perfect)&lt;/td&gt;
&lt;td&gt;0.941&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.957&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toxic comments auto-actioned at ≥ 90% precision&lt;/td&gt;
&lt;td&gt;30.9%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comments sent to human review (to catch ~80% of toxic ones)&lt;/td&gt;
&lt;td&gt;10.7%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8.5%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Thresholds are policy, not maths
&lt;/h2&gt;

&lt;p&gt;A model only gives scores. Someone has to decide where the lines go. I used two:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-action&lt;/strong&gt; only where the model is at least 90% precise, because wrongly removing a comment is costly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send to review&lt;/strong&gt; low enough to catch about 80% of toxic comments, with humans checking everything in between.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the modern model, that means about 3% of comments are actioned automatically and 8.5% go to reviewers. That's about 20% less review work than the baseline for the same coverage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't count the model's flags
&lt;/h2&gt;

&lt;p&gt;How much toxic content is there? Counting the model's flags would say 11.7%. A random sample of 1,000 comments, the way you'd do it with human reviewers, estimates &lt;strong&gt;7.4%&lt;/strong&gt; (95% interval 5.9–9.2%), and the true figure is 8.0%. The small sample wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fairness check
&lt;/h2&gt;

&lt;p&gt;Here's the finding that matters most. Among &lt;strong&gt;harmless&lt;/strong&gt; comments that simply mention a group, how many get wrongly flagged?&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comment mentions&lt;/th&gt;
&lt;th&gt;Simple baseline&lt;/th&gt;
&lt;th&gt;DistilBERT&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(average, identity-annotated comments)&lt;/td&gt;
&lt;td&gt;12.8%&lt;/td&gt;
&lt;td&gt;9.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Black&lt;/td&gt;
&lt;td&gt;49.6%&lt;/td&gt;
&lt;td&gt;35.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;white&lt;/td&gt;
&lt;td&gt;44.3%&lt;/td&gt;
&lt;td&gt;31.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gay or lesbian&lt;/td&gt;
&lt;td&gt;38.6%&lt;/td&gt;
&lt;td&gt;29.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Muslim&lt;/td&gt;
&lt;td&gt;37.2%&lt;/td&gt;
&lt;td&gt;25.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;transgender&lt;/td&gt;
&lt;td&gt;31.6%&lt;/td&gt;
&lt;td&gt;24.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The modern model is fairer for every group, but harmless comments mentioning some groups are still flagged up to about four times more often than average. The model has partly learned that &lt;em&gt;naming&lt;/em&gt; a group is a sign of toxicity.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;What surprised me most:&lt;/strong&gt; harmless comments that simply &lt;em&gt;mention&lt;/em&gt; a group get flagged far more often. With the simple model, half of the harmless comments mentioning "Black" were sent to review (50%), against 13% of harmless comments overall. The modern model shrank that gap, but it didn't close it. A single headline score (above 0.95 out of 1) hides this completely, so I would always break results down by group before trusting a moderation model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd do next:&lt;/strong&gt; tackle that bias directly. I'd add more harmless training examples that mention these groups, track the false-positive rate for each group separately, and only call the model "better" when those gaps shrink.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://huggingface.co/spaces/Malavnaik/moderation-classifier-demo" rel="noopener noreferrer"&gt;demo&lt;/a&gt; runs a compressed copy of the model in your browser, so nothing you type leaves your device. Move the threshold and watch decisions change. It's a portfolio demo, not a production moderation system.&lt;/p&gt;

&lt;p&gt;For the record, a late submission to the original Kaggle competition scored 0.915 on its bias-aware metric. The competition closed years ago, so that's a score only, with no ranking.&lt;/p&gt;

</description>
      <category>trustandsafety</category>
      <category>datascience</category>
      <category>portfolio</category>
      <category>python</category>
    </item>
    <item>
      <title>Migrating On-Prem SQL Server to Azure SQL PaaS: How We Cut Infrastructure Costs by 25%</title>
      <dc:creator>Malavnaik</dc:creator>
      <pubDate>Thu, 15 Oct 2026 11:43:47 +0000</pubDate>
      <link>https://dev.to/malavnaik_0381711024683d4/migrating-on-prem-sql-server-to-azure-sql-paas-how-we-cut-infrastructure-costs-by-25-2oo9</link>
      <guid>https://dev.to/malavnaik_0381711024683d4/migrating-on-prem-sql-server-to-azure-sql-paas-how-we-cut-infrastructure-costs-by-25-2oo9</guid>
      <description>&lt;p&gt;At S&amp;amp;P Global Market Intelligence I helped migrate on-premise SQL Server databases to Azure SQL PaaS. Our team moved between 10 and 50 databases to Azure SQL Managed Instance. The headline result was a reduction in infrastructure costs of about 25%. The more important result was that the people using those databases barely noticed it happened.&lt;/p&gt;

&lt;p&gt;Here's what I learned about doing a migration like that well.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Inventory before you plan
&lt;/h2&gt;

&lt;p&gt;Before choosing a target, list everything that touches the database: ETL jobs, reports, linked servers, SQL Agent jobs, cross-database queries, CLR assemblies. PaaS options don't support every on-prem feature in the same way, and you want to find the gaps on a spreadsheet, not in production.&lt;/p&gt;

&lt;p&gt;Microsoft's Data Migration Assistant and Azure Migrate help here, but they don't know about the Excel workbook someone in finance refreshes every Monday. Talk to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Pick the right PaaS flavour
&lt;/h2&gt;

&lt;p&gt;Azure SQL Database and Azure SQL Managed Instance trade compatibility against simplicity. Managed Instance keeps more instance-level features (Agent jobs, cross-database queries), so it suits lift-and-shift. Single databases are simpler and can be cheaper, but may need refactoring. Decide per workload, not per estate. We went with Managed Instance, which kept most instance-level features in place and meant fewer changes to existing code.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Right-size, don't copy
&lt;/h2&gt;

&lt;p&gt;Most of the savings came from not paying for capacity we didn't use. On-prem servers are sized for peak load plus growth plus fear. In the cloud you can measure actual usage and scale. Look at real CPU, memory and IO over several weeks before choosing service tiers.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Migrate in waves
&lt;/h2&gt;

&lt;p&gt;Start with low-risk databases: development, reporting copies, internal tools. Each wave teaches you something about networking, permissions or performance that makes the next wave smoother.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Test the boring things
&lt;/h2&gt;

&lt;p&gt;Performance gets attention. In our migration, the real problems came from elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connection strings hard-coded in old applications and reports&lt;/li&gt;
&lt;li&gt;SSIS packages and SQL Agent jobs that needed reworking to run against Azure&lt;/li&gt;
&lt;li&gt;Jobs that relied on local file paths on the old servers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also worth checking: time zone and collation differences, firewall rules and private endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Define "done" with the users
&lt;/h2&gt;

&lt;p&gt;A migration isn't finished when the data moves. It's finished when every report, job and application works as before and the old servers are switched off. Agree that checklist with stakeholders up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Cloud migration savings come mostly from right-sizing and retiring infrastructure. Keeping users happy comes from inventory, testing and communication. Neither is glamorous, and both matter more than the migration tooling.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>sql</category>
      <category>dataengineering</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Hi, I'm Malav Naik: from Gujarat to Dublin, building AI systems and VEDASRA</title>
      <dc:creator>Malavnaik</dc:creator>
      <pubDate>Wed, 14 Oct 2026 20:06:04 +0000</pubDate>
      <link>https://dev.to/malavnaik_0381711024683d4/hi-im-malav-naik-from-gujarat-to-dublin-building-ai-systems-and-vedasra-1ech</link>
      <guid>https://dev.to/malavnaik_0381711024683d4/hi-im-malav-naik-from-gujarat-to-dublin-building-ai-systems-and-vedasra-1ech</guid>
      <description>&lt;p&gt;I'm &lt;strong&gt;Malav Naik&lt;/strong&gt;, an AI systems builder based in &lt;strong&gt;Dublin, Ireland&lt;/strong&gt;, and the creator of &lt;strong&gt;VEDASRA&lt;/strong&gt;, an anime-style project. This is my first post here, so here's who I am, what I build, and what I'll be writing about.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Gujarat to Dublin
&lt;/h2&gt;

&lt;p&gt;I grew up in Gujarat, India, and studied for a Bachelor of Computer Applications at Veer Narmad South Gujarat University. My career started in data: for over five years I worked at &lt;strong&gt;S&amp;amp;P Global Market Intelligence&lt;/strong&gt; as a Data Analyst &amp;amp; ETL Developer, building SQL pipelines, migrating databases to the cloud, and making sure business intelligence reports were built on data people could trust.&lt;/p&gt;

&lt;p&gt;In September 2024 I moved to Dublin to do an &lt;strong&gt;MSc in Data Analytics at the National College of Ireland&lt;/strong&gt;. I graduated in 2025 with a 2:1. My dissertation looked at how &lt;strong&gt;knowledge graphs and prompt engineering&lt;/strong&gt; can support AI-driven longitudinal analytics: in short, giving language models structured facts to reason over instead of letting them guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I build now
&lt;/h2&gt;

&lt;p&gt;Today I'm a BIS Analyst / Applications Analyst &amp;amp; Developer at &lt;strong&gt;Trinity Care Nursing Homes&lt;/strong&gt; in Dublin. I design AI-driven automation and analytics that support 13 nursing homes: consolidating operational reporting, integrating business systems, and testing an AI integration with Oracle NetSuite that lets authorised users ask financial questions in plain English, with role-based access and human approval for anything sensitive.&lt;/p&gt;

&lt;p&gt;The lesson I keep relearning: &lt;strong&gt;AI is only as reliable as the data engineering underneath it.&lt;/strong&gt; Most of the work is validation, audit logs, exception handling and deciding where a human should stay in the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  A project I'm proud of: SkillBot
&lt;/h2&gt;

&lt;p&gt;During my MSc I built &lt;strong&gt;SkillBot&lt;/strong&gt;, a skill-recommendation chatbot that combines a fine-tuned GPT-3.5 model, retrieval-augmented generation (RAG) and a knowledge graph of about &lt;strong&gt;2.3 million RDF triples&lt;/strong&gt;. Grounding answers in the graph got it to &lt;strong&gt;97% factual accuracy&lt;/strong&gt; in its reported evaluation and noticeably reduced hallucinations. I'll write a full breakdown soon.&lt;/p&gt;

&lt;h2&gt;
  
  
  VEDASRA
&lt;/h2&gt;

&lt;p&gt;Outside my day job I'm building &lt;strong&gt;VEDASRA&lt;/strong&gt;, an anime-style creative project, here in Dublin. More on that in a future post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'll write about
&lt;/h2&gt;

&lt;p&gt;Every week I'll share something real from my work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How SkillBot combines RAG with a knowledge graph&lt;/li&gt;
&lt;li&gt;What my dissertation taught me about prompt engineering&lt;/li&gt;
&lt;li&gt;Lessons from five years of enterprise ETL&lt;/li&gt;
&lt;li&gt;Designing AI automations that ask before they act&lt;/li&gt;
&lt;li&gt;Behind the scenes of VEDASRA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're working on AI agents, RAG or data engineering, especially in Ireland, I'd love to connect.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Malav Naik · Dublin · creator of VEDASRA&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Portfolio: &lt;a href="https://malavnaik99.github.io/" rel="noopener noreferrer"&gt;malavnaik99.github.io&lt;/a&gt; · GitHub: &lt;a href="https://github.com/malavnaik99" rel="noopener noreferrer"&gt;malavnaik99&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>database</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
