<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Case-Studies on Vedant Andhale</title>
    <link>https://www.vedant.me/case-studies/</link>
    <description>Recent content in Case-Studies on Vedant Andhale</description>
    <image>
      <url>https://www.vedant.me/</url>
      <link>https://www.vedant.me/</link>
    </image>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Thu, 10 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.vedant.me/case-studies/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>AmbitionBox salary scraper: from embedded page data to usable records</title>
      <link>https://www.vedant.me/projects/ambitionbox-scraper/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/projects/ambitionbox-scraper/</guid>
      <description>A Python salary-data pipeline with a custom Nuxt payload parser, HTML fallback, batch collection and pandas-based cleaning.</description>
      <content:encoded><![CDATA[<p>Salary comparisons need more context than a role name and a number. Company, location, experience range and the number of reports all affect how a record should be read.</p>
<p>I built the AmbitionBox salary scraper to collect that context into a consistent dataset. The Python pipeline discovers companies, extracts role-level salary records, writes batches and merges them into an analysis-ready CSV.</p>
<h2 id="the-parsing-work">The parsing work</h2>
<p>The most interesting part was the page&rsquo;s embedded Nuxt data. It is not a plain JSON object: the payload uses a self-invoking JavaScript function, arguments and variable references to construct records.</p>
<p>The parser identifies the function boundaries, tracks nested delimiters and quoted strings, maps parameter names to argument values, and resolves the property assignments used by the job-profile records. It extracts the supported structure rather than executing the page&rsquo;s JavaScript.</p>
<p>That distinction also defines a limitation: this is a parser for an observed payload format, not a general JavaScript interpreter. A change in the source representation can require an update.</p>
<h2 id="a-fallback-with-honest-missing-values">A fallback with honest missing values</h2>
<p>If the structured extraction returns no role records, the scraper falls back to the HTML table. That path can recover visible salary ranges and experience information, while leaving unavailable average-salary values empty.</p>
<p>An absent average should not become zero or an invented midpoint. Keeping it missing lets the later analysis distinguish between “this value was reported” and “this field was unavailable.”</p>
<table>
	<thead>
			<tr>
					<th>Stage</th>
					<th>Responsibility</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Discovery</td>
					<td>Build the company list used by subsequent batches.</td>
			</tr>
			<tr>
					<td>Extraction</td>
					<td>Parse role records, with optional location and role filters.</td>
			</tr>
			<tr>
					<td>Batch execution</td>
					<td>Split work through a GitHub Actions matrix and collect CSV artifacts.</td>
			</tr>
			<tr>
					<td>Merge</td>
					<td>Deduplicate records and normalise numeric and text columns with pandas.</td>
			</tr>
			<tr>
					<td>Derived fields</td>
					<td>Calculate salary in lakhs, experience midpoint and salary-range width.</td>
			</tr>
	</tbody>
</table>
<h2 id="give-the-merged-dataset-a-clear-identity">Give the merged dataset a clear identity</h2>
<p>The merge step uses company slug, role slug and location as the deduplication key. It keeps the last matching record in the concatenated input, converts numeric fields and sorts the output for inspection.</p>
<p>That is a concrete policy, not proof that a retained row is the newest observation. Adding an explicit collection timestamp would make that distinction easier to handle in future versions.</p>
<p>The workflow can collect available batch artifacts even when some jobs fail. A merged file therefore needs a completeness check before being treated as full coverage. I would add an expected-versus-received batch manifest and parser success counts as the next operational improvements.</p>
<h2 id="what-is-complete">What is complete</h2>
<p>The repository contains the collection, parsing and merge pipeline. Its exploratory notebook and interactive dashboard are described as work in progress, so I do not present them as finished deliverables.</p>
<p>This is an educational data-engineering project. The dataset reflects the source&rsquo;s salary reports; it is not verified payroll data or a representative survey of every employer. The portfolio value is the engineering: handling a nontrivial source format, preserving missing values and producing records that can be inspected and analysed.</p>
<p>Implementation details: <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/src/scraper/parser.py">payload parser</a>, <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/src/scraper/salary.py">salary collection</a>, <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/scripts/merge_data.py">merge logic</a>, and <a href="https://github.com/VedantAndhale/amitionbox_salary_scraper/blob/main/.github/workflows/scrape.yml">batch workflow</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Crop Cure: a grape-leaf classifier inside WhatsApp</title>
      <link>https://www.vedant.me/projects/crop-cure/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/projects/crop-cure/</guid>
      <description>A research prototype connecting a PyTorch image classifier to a multilingual WhatsApp workflow with FastAPI.</description>
      <content:encoded><![CDATA[<p>A model that accepts a tensor is useful to another developer. A model that accepts a photo in a familiar chat interface is easier for someone else to try.</p>
<p>Crop Cure connects a grape-leaf classifier to WhatsApp. A user chooses a language, sends a photograph, and receives a response based on the predicted class. The project supports English, Hindi and Marathi response flows.</p>
<h2 id="the-workflow">The workflow</h2>
<table>
	<thead>
			<tr>
					<th>Step</th>
					<th>Implementation</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Receive a message</td>
					<td>FastAPI and <code>pywa_async</code> handle the WhatsApp flow.</td>
			</tr>
			<tr>
					<td>Prepare the image</td>
					<td>Convert to RGB, resize, centre-crop and normalise.</td>
			</tr>
			<tr>
					<td>Run inference</td>
					<td>A custom PyTorch residual network with channel and spatial attention runs on the CPU.</td>
			</tr>
			<tr>
					<td>Decide whether to answer</td>
					<td>Compare the top softmax score with the configured threshold.</td>
			</tr>
			<tr>
					<td>Return the response</td>
					<td>Use the selected language and the predicted class.</td>
			</tr>
	</tbody>
</table>
<p>My work here spans the classifier integration and the surrounding application: getting an image from a message into the model, interpreting the output, and turning that output into a usable reply.</p>
<p>The model has four labels: black rot, esca, leaf blight and healthy. That is a deliberately narrow scope. It is not a general classifier for every crop or every possible leaf condition.</p>
<h2 id="a-threshold-needs-an-honest-description">A threshold needs an honest description</h2>
<p>The implementation uses a <code>0.98</code> confidence threshold. Predictions below it return <code>Unclassified</code>.</p>
<p>That number is <strong>not 98% accuracy</strong>. It is a threshold applied to a model output. A high softmax score can still be wrong, especially when the photograph differs from the data used to train the model. Lighting, background, camera distance and an unsupported plant all make the input harder to interpret.</p>
<p>The fallback gives the application a way to avoid returning a class for every image. Whether it rejects enough unsuitable images requires evaluation with representative photographs; the public repository does not establish field accuracy.</p>
<h2 id="where-the-prototype-stops">Where the prototype stops</h2>
<p>The repository describes the project as under development and not actively maintained. It is best understood as a research prototype, rather than a deployed agricultural diagnostic service.</p>
<p>Language preferences currently live in an in-memory dictionary, so they do not survive a process restart. Inference also runs synchronously inside the message-handling path. If I continued the project, I would persist conversation state, move inference into a controlled worker path, and evaluate class-specific errors and rejection behaviour on a held-out field dataset.</p>
<p>The interesting engineering lesson is how much of the product sits around the model. The preprocessing contract, supported inputs, fallback behaviour and message flow all determine whether the prediction can be used sensibly.</p>
<p>Implementation details: <a href="https://github.com/VedantAndhale/Crop_Cure_Bot/blob/main/src/model.py">model and preprocessing</a> and <a href="https://github.com/VedantAndhale/Crop_Cure_Bot/blob/main/main.py">WhatsApp application</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>FreightSense: making shipment recommendations inspectable</title>
      <link>https://www.vedant.me/projects/freightsense/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/projects/freightsense/</guid>
      <description>A FastAPI decision-support prototype combining deterministic shipping calculations, LLM recommendations and a human override history.</description>
      <content:encoded><![CDATA[<p>A delayed shipment creates a practical question: should someone expedite it, offer a discount, or wait? A useful answer needs to account for the order&rsquo;s economics and explain the recommendation well enough for a person to challenge it.</p>
<p>FreightSense is my prototype for that workflow. It puts a rules-based calculation beside an LLM assessment, records both, and leaves the final decision with the operator.</p>
<h2 id="what-i-built">What I built</h2>
<p>The application combines a Python/FastAPI API, a small browser dashboard, a deterministic evaluation layer, Groq-hosted LLM calls and SQLite records. The repository also includes Docker and Cloud Run deployment configuration.</p>
<table>
	<thead>
			<tr>
					<th>Stage</th>
					<th>Responsibility</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Shipment input</td>
					<td>Collect the order, shipping and estimated delivery details.</td>
			</tr>
			<tr>
					<td>Deterministic evaluation</td>
					<td>Calculate delay, financial exposure and a weighted risk score; apply recommendation rules.</td>
			</tr>
			<tr>
					<td>LLM evaluation</td>
					<td>Interpret the same context and return a structured assessment.</td>
			</tr>
			<tr>
					<td>Comparison</td>
					<td>Show whether the two recommendations agree.</td>
			</tr>
			<tr>
					<td>Decision record</td>
					<td>Store the evaluation and any later human overrides.</td>
			</tr>
	</tbody>
</table>
<h2 id="keep-the-arithmetic-outside-the-prompt">Keep the arithmetic outside the prompt</h2>
<p>The deterministic layer calculates delay and exposure before the model sees the request. Its risk score combines delay severity, historical context, financial exposure and margin. The recommendation itself follows separate business rules: no delay can mean <code>NO_ACTION</code>; a feasible and economically acceptable intervention can mean <code>EXPEDITE</code>; otherwise the result can be <code>DISCOUNT</code> or <code>MONITOR</code>.</p>
<p>That distinction matters. A risk score describes the situation. It does not, by itself, prove that paying to expedite is sensible. The code checks conditions such as the shipping mode and the estimated intervention cost.</p>
<p>The LLM receives these calculations as context. The application parses its response, checks the recommendation label and bounds the confidence value. It also handles several failure paths, including API errors and invalid JSON, by returning the deterministic result with the LLM marked unavailable.</p>
<h2 id="disagreement-is-useful-information">Disagreement is useful information</h2>
<p>A model disagreeing with the rules should be visible. FreightSense stores the two assessments and flags the mismatch instead of quietly replacing one with the other.</p>
<p>The override endpoint lets a person record a different decision, a reason and outcome notes. Multiple overrides are retained as separate entries. That history makes it possible to revisit why an operator intervened, rather than seeing only the latest choice.</p>
<h2 id="what-the-prototype-establishes">What the prototype establishes</h2>
<p>The repository demonstrates the complete evaluation-and-review path. It does <strong>not</strong> establish a measured reduction in shipping costs. The financial figures are estimates, and the model&rsquo;s confidence is a self-reported value, not a calibrated probability of correctness.</p>
<p>For a production version, my next priorities would be durable shared storage, access control, stronger validation of model outputs, and a labelled evaluation set that includes expensive mistakes and ambiguous cases. Those are more useful next steps than adding another model to the comparison.</p>
<p>Implementation details: <a href="https://github.com/VedantAndhale/FreightSense/blob/main/app/core/deterministic.py">deterministic rules</a>, <a href="https://github.com/VedantAndhale/FreightSense/blob/main/app/core/llm_evaluator.py">LLM evaluation</a>, and <a href="https://github.com/VedantAndhale/FreightSense/blob/main/app/api/routes.py">API routes</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>loadfile: one call to load tabular data</title>
      <link>https://www.vedant.me/projects/loadfile/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/projects/loadfile/</guid>
      <description>A small Python package that loads local and cloud files into pandas through a consistent API, replacing repeated file-loading code.</description>
      <content:encoded><![CDATA[<p>I built <strong>loadfile</strong> to stop copying the same file-loading code between scripts. It provides one entry point for tabular data, whether the file is on local disk or in cloud storage.</p>
<h2 id="one-small-api">One small API</h2>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">loadfile</span> <span class="kn">import</span> <span class="n">load</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;data/local.csv&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;gs://my-bucket/data.parquet&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;archive.zip&#34;</span><span class="p">,</span> <span class="n">filename</span><span class="o">=</span><span class="s2">&#34;sales.csv&#34;</span><span class="p">)</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The function is named <code>load()</code>. The package also exports <code>load_data()</code> as a backwards-compatible alias.</p>
<p><code>fsspec</code> selects the storage backend from the path prefix. The package selects the reader from the file extension, or an explicit <code>format=</code> argument, and passes reader options through to pandas. Cloud backends require their corresponding optional dependencies and credentials.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;export.tsv&#34;</span><span class="p">,</span> <span class="nb">format</span><span class="o">=</span><span class="s2">&#34;csv&#34;</span><span class="p">,</span> <span class="n">sep</span><span class="o">=</span><span class="s2">&#34;</span><span class="se">\t</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">df</span> <span class="o">=</span> <span class="n">load</span><span class="p">(</span><span class="s2">&#34;large.csv&#34;</span><span class="p">,</span> <span class="n">fast</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">usecols</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;id&#34;</span><span class="p">,</span> <span class="s2">&#34;value&#34;</span><span class="p">])</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>CSV, Parquet, JSON, Excel and Feather share the same interface. <code>fast=True</code> opts into Arrow-backed reading; it changes the reading defaults rather than promising a fixed speedup for every file.</p>
<h2 id="zip-files-without-another-helper">ZIP files without another helper</h2>
<p>A ZIP containing one supported data file returns a DataFrame. Multiple supported members return a dictionary keyed by filename. You can select one member by name or pass a list to load a subset.</p>
<p>The implementation reads ZIP contents into memory, so archive size still matters. The aim is a convenient reusable loader, not an out-of-core processing engine.</p>
<p>Source: <a href="https://github.com/VedantAndhale/loadfile/blob/main/src/loadfile/__init__.py">public API</a> and <a href="https://github.com/VedantAndhale/loadfile/blob/main/src/loadfile/core.py">loading implementation</a>.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Reading 75,000&#43; consumer complaints in Power BI</title>
      <link>https://www.vedant.me/projects/consumer-complaints/</link>
      <pubDate>Thu, 10 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://www.vedant.me/projects/consumer-complaints/</guid>
      <description>A Power BI portfolio project exploring complaint volume, response timeliness and disputes across products, regions and time.</description>
      <content:encoded><![CDATA[<p>A total complaint count is a starting point. It does not tell you which products generate the complaints, whether responses arrive on time, or how often customers dispute the outcome.</p>
<p>This dashboard brings those questions into one Power BI report. It uses a historical consumer-complaints dataset containing more than 75,000 records, with views across products, geography and time.</p>
<h2 id="start-with-the-questions">Start with the questions</h2>
<table>
	<thead>
			<tr>
					<th>Question</th>
					<th>Report view</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Where is the workload concentrated?</td>
					<td>Complaint counts by product, issue and geography.</td>
			</tr>
			<tr>
					<td>Are responses timely?</td>
					<td>Timely-response and in-progress indicators.</td>
			</tr>
			<tr>
					<td>What happens after a response?</td>
					<td>Dispute and resolution measures.</td>
			</tr>
			<tr>
					<td>Is the pattern changing?</td>
					<td>Monthly views and filters.</td>
			</tr>
	</tbody>
</table>
<p>I built the dashboard to make these measures explorable together. Someone looking at one product can inspect its volume and response outcomes without rebuilding the analysis from a spreadsheet each time.</p>
<p>The repository includes the Power BI file, the source CSV and a dashboard image. That makes the analytical artifact available to inspect, rather than leaving the project at a screenshot alone.</p>
<h2 id="counts-need-context">Counts need context</h2>
<p>A product with the most complaints is not automatically the worst-performing product. It may also have the most customers. The complaint dataset alone does not provide every denominator needed to make that comparison.</p>
<p>Rates need equally careful reading. A timely-response percentage and a dispute percentage answer different questions; neither is a substitute for understanding the underlying cases. Filters change the population being examined, so the selected product and period are part of the interpretation.</p>
<p>This is why I would present the report as a way to locate patterns worth investigating. It does not establish the cause of a complaint or prove that an operational intervention worked.</p>
<h2 id="what-i-would-add-next">What I would add next</h2>
<p>The next useful addition would be a short metric dictionary beside the report: each measure&rsquo;s numerator, denominator, exclusions and treatment of missing values. I would also make the dataset&rsquo;s coverage period and refresh status more prominent.</p>
<p>Those additions would help another person reproduce the reading of a chart. For an analytical portfolio project, that is a stronger improvement than adding more visuals to the same page.</p>
<p>The published result is an interactive report on historical data. No real-time refresh or measured business impact is claimed here.</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
