<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[From Idea to Code]]></title><description><![CDATA[From Idea to Code]]></description><link>https://from-idea-to-code.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 25 Sep 2026 11:04:37 GMT</lastBuildDate><atom:link href="https://from-idea-to-code.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[From Jupyter Notebooks to Production: Mistakes I Made Building an ML App]]></title><description><![CDATA[I built an employee attrition predictor. Random Forest, 83% accuracy, clean notebooks. Wrapped it in FastAPI, tested it... and it predicted "Stay" for everyone.
This is what I learned debugging my way]]></description><link>https://from-idea-to-code.hashnode.dev/from-jupyter-notebooks-to-production-mistakes-i-made-building-an-ml-app</link><guid isPermaLink="true">https://from-idea-to-code.hashnode.dev/from-jupyter-notebooks-to-production-mistakes-i-made-building-an-ml-app</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[FastAPI]]></category><category><![CDATA[gradio]]></category><category><![CDATA[Random Forest]]></category><dc:creator><![CDATA[Deepika]]></dc:creator><pubDate>Wed, 25 Feb 2026 05:23:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/692d6508a721e1c7f31351cd/dc7911f4-7dc6-4867-b3d6-282ba6cdec99.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I built an employee attrition predictor. Random Forest, 83% accuracy, clean notebooks. Wrapped it in FastAPI, tested it... and it predicted "Stay" for everyone.</p>
<p>This is what I learned debugging my way from notebooks to production.</p>
<h3>The Setup</h3>
<p><strong>Goal</strong>: Predict if employees will leave + explain why with SHAP.<br /><strong>Stack</strong>: Random Forest, FastAPI, Gradio, SHAP</p>
<p>Simple enough. Here's where it broke.</p>
<h2>Mistake #1: Missing The Pattern in EDA</h2>
<p>During exploratory analysis, I found something crucial: <strong>62% attrition at Year 5</strong>.<br />Employees' satisfaction dropped in Year 4 (0.43), then they left in Year 5. There's a lag—people don't quit when unhappy, they quit <em>after</em> stewing in it.  </p>
<p>Also discovered:</p>
<ul>
<li><p>Only 2% get promoted</p>
</li>
<li><p>Promoted employees rarely leave (95% retention)</p>
</li>
<li><p>Even high-salary employees leave without career growth</p>
</li>
</ul>
<p><strong>The insight</strong>: Attrition follows a predictable career stagnation pattern. Promotions matter more than salary.</p>
<p>This shaped my approach to thresholds and risk levels later.</p>
<h2>Mistake #2: The Threshold Trap</h2>
<p>Back to my "everyone stays" problem during FastAPI testing.<br />Model metrics looked perfect: 83% accuracy, good recall/precision, balanced classes. But predictions were always "Stay."</p>
<p>Then I checked probabilities:</p>
<pre><code class="language-python">prediction_probability = pipeline.predict_proba(input_df)[0]
# [0.52, 0.48]  ← Wait, what?
</code></pre>
<p>52% Stay, 48% Leave → Predicts "Stay"</p>
<p><strong>The problem</strong>: Sklearn uses 0.5 threshold. To an engineer, 52% is correct. To HR, an employee with 48% leave risk needs attention.</p>
<p><strong>Prediction ≠ Business Risk</strong></p>
<p>I experimented with lower thresholds (0.3-0.4) in notebooks but took a different approach in the API:</p>
<pre><code class="language-python">prediction = "Leave" if pred == 1 else "Stay"

# But also calculate risk independently
if prediction_probability[1] &lt; 0.4:
    risk_level = 'Low'
elif prediction_probability[1] &lt; 0.7:
    risk_level = 'Medium'
else:
    risk_level = 'High'
</code></pre>
<p>Now my API returns:</p>
<pre><code class="language-json">{
  "prediction": "Stay",
  "leave_probability": 0.48,
  "risk_level": "Medium"
}
</code></pre>
<p><strong>The lesson</strong>: Model output is technical. Business decisions need context.</p>
<p>Once I sorted this out in the backend, I was ready to build the frontend with Gradio.</p>
<h2>Mistake #3: SHAP Integration</h2>
<p>SHAP assigns each feature a contribution score. Positive = increases leave risk, negative = decreases it. Simple concept, messy implementation.</p>
<p><strong>Problem 1: Version issues</strong></p>
<p>Tried plotting:</p>
<pre><code class="language-python">shap.summary_plot(shap_values, features)
</code></pre>
<p>Got cryptic NumPy errors. SHAP had a version update that broke old methods.</p>
<p><strong>Solution</strong>: Skipped plots, extracted raw values:</p>
<pre><code class="language-python"># Get SHAP values for class 1 (Leave)
shap_values = explainer(processed_input)
shap_class1 = shap_values.values[0, :, 1]  # Binary classification

# Pair with feature names
feature_names = pipeline.named_steps["preprocess"].get_feature_names_out()
feature_impacts = list(zip(feature_names, shap_class1))

# Sort by absolute impact
top_features = sorted(feature_impacts, key=lambda x: abs(x[1]), reverse=True)[:3]
</code></pre>
<p><strong>Problem 2: Making it readable</strong></p>
<p>Raw output: <code>remainder__satisfaction_level: -0.104</code></p>
<p>HR doesn't care about "remainder__" prefixes. So I cleaned it:</p>
<pre><code class="language-python">for feature, value in top_features:
    clean_name = feature.split("__")[-1]  # Drop pipeline prefix
    effect = "increases_leave_risk" if value &gt; 0 else "decreases_leave_risk"
    top_factors.append({
        "feature": clean_name,
        "impact": effect,
        "contribution_strength": round(float(value), 3)
    })
</code></pre>
<p>Gradio now shows:</p>
<pre><code class="language-plaintext">↓ satisfaction_level (-0.104)
↑ time_spend_company (0.049)
↓ last_evaluation (-0.101)
</code></pre>
<p><strong>The lesson</strong>: Explainability is translation, not just computation.</p>
<h2>Mistake #4: Path &amp; Host Issues</h2>
<p><strong>Paths breaking deployment:</strong></p>
<pre><code class="language-python">pipeline = joblib.load("experiments/my_pipeline.joblib")
</code></pre>
<p>Relative paths depend on where you run the command. Fixed with:</p>
<pre><code class="language-python">from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
MODEL_PATH = BASE_DIR.parent / "experiments" / "my_pipeline.joblib"
</code></pre>
<p><strong>Host confusion:</strong></p>
<p>Set Gradio to <code>server_name="0.0.0.0"</code>. Terminal showed <code>http://0.0.0.0:7860</code>. Clicked it—didn't work.</p>
<p>Browsers can't connect to <code>0.0.0.0</code>. Use <code>http://127.0.0.1:7860</code> instead.</p>
<p>Small thing, but it ate 30 minutes of my life.</p>
<h2>The Final App</h2>
<img src="https://cdn.hashnode.com/uploads/covers/692d6508a721e1c7f31351cd/b2f84a5c-9bad-413f-b369-36bac08fe1f1.png" alt="" style="display:block;margin:0 auto" />

<img src="https://cdn.hashnode.com/uploads/covers/692d6508a721e1c7f31351cd/b6cd4d4a-b3da-49a7-8d1d-f3ea29cdadc9.png" alt="" style="display:block;margin:0 auto" />

<p>HR enters data, gets:</p>
<ul>
<li><p>Prediction (Stay/Leave)</p>
</li>
<li><p>Leave probability (10%)</p>
</li>
<li><p>Risk level (Low/Medium/High)</p>
</li>
<li><p>Top 3 factors with SHAP values</p>
</li>
</ul>
<p>Clean, actionable, explainable.</p>
<h2>What I Learned</h2>
<p><strong>Before</strong>: Does my model have good accuracy?<br /><strong>After</strong>: Does it solve the business problem?</p>
<p><strong>Before</strong>: Prediction is correct if it matches the label<br /><strong>After</strong>: Prediction ≠ risk. Context matters.</p>
<p><strong>Before</strong>: SHAP values are just numbers<br /><strong>After</strong>: Explainability is translation for humans</p>
<p><strong>Before</strong>: Relative paths work for me<br /><strong>After</strong>: Production code must work anywhere</p>
<h2>Key Takeaways</h2>
<ol>
<li><p><strong>Domain knowledge shapes everything.</strong> Year 5 attrition spike and promotion patterns directly influenced my threshold and risk decisions.</p>
</li>
<li><p><strong>Prediction ≠ Business Decision.</strong> 48% leave probability might be "Stay" but it's a retention risk HR needs to see.</p>
</li>
<li><p><strong>Explainability is for people.</strong> SHAP values mean nothing until you translate them into actionable insights.</p>
</li>
<li><p><strong>Test in production mode.</strong> Paths and hosts that work locally often break in deployment.</p>
</li>
<li><p><strong>Systems &gt; Models.</strong> Training took hours. Building a usable, explainable API took days.</p>
</li>
</ol>
<h2>Final Thoughts</h2>
<p>This project taught me more than any tutorial could. Not because I built a perfect system, but because I hit every wall and learned to climb over it.</p>
<p>The threshold trap, SHAP integration struggles, the gap between predictions and business risk—none of this was in any course syllabus. But these are exactly the lessons that separate models that train from systems that matter.</p>
<p>If you're building something similar and hit these same problems, you're not failing. You're learning the parts that actually count.</p>
<p>Got questions? Found better solutions? Hit similar walls? Drop your thoughts in the comments—I'd love to hear your war stories.</p>
<p><a href="https://github.com/Deepika081/employee-attrition-app"><strong>View the complete source code and implementation on GitHub</strong></a></p>
]]></content:encoded></item><item><title><![CDATA[Building a Resume Screener: What Actually Worked (and What Completely Broke)]]></title><description><![CDATA[Why read this?
When I first thought about building a resume screener, the idea felt too simple.
Scan the resume.Scan the job description.Compute cosine similarity.Done. Right?
To sanity-check myself, I asked ChatGPT to behave like a senior ML enginee...]]></description><link>https://from-idea-to-code.hashnode.dev/building-a-resume-screener-what-actually-worked-and-what-completely-broke</link><guid isPermaLink="true">https://from-idea-to-code.hashnode.dev/building-a-resume-screener-what-actually-worked-and-what-completely-broke</guid><category><![CDATA[nlp]]></category><category><![CDATA[Portfolio Project]]></category><category><![CDATA[Learning Journey]]></category><dc:creator><![CDATA[Deepika]]></dc:creator><pubDate>Thu, 29 Jan 2026 10:16:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769681217352/3a14bc8e-048c-4f73-bef7-e601db81310b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-why-read-this">Why read this?</h2>
<p>When I first thought about building a resume screener, the idea felt <em>too</em> simple.</p>
<p>Scan the resume.<br />Scan the job description.<br />Compute cosine similarity.<br />Done. Right?</p>
<p>To sanity-check myself, I asked ChatGPT to behave like a senior ML engineer and review the idea. Instead of encouragement, I got slapped with reality:</p>
<p><em>“Cosine similarity alone won’t work. You need segmentation.”</em></p>
<p>Rude? Maybe.<br />Correct? Absolutely.</p>
<p>That single statement turned what I thought was a weekend project into hours of debugging, hair-pulling, and multiple interrogation sessions with GPT. If that sounds familiar, this article is for you.</p>
<p>I’ll walk you through <strong>what I built</strong>, <strong>what broke</strong>, and <strong>what finally worked</strong>, so you can save time — and probably some hair.</p>
<h2 id="heading-the-core-idea">The core idea</h2>
<p>I split the problem into two independent parts:</p>
<h3 id="heading-1-resume-processing-hard-part">1. Resume processing (hard part)</h3>
<p>Resumes are messy, inconsistent, and unstructured. So I used a <strong>rule-based segmentation approach inspired by TSHD (Topic Segmentation based on Heading Detection)</strong>.</p>
<h3 id="heading-2-job-description-processing-easier-part">2. Job Description processing (easier part)</h3>
<p>Job descriptions are comparatively structured. For them, <strong>sentence embeddings work surprisingly well</strong>, so I leaned on that.</p>
<p>This separation alone made the system much easier to reason about.</p>
<h2 id="heading-step-1-resume-preprocessing">Step 1: Resume preprocessing</h2>
<p>Before any intelligence, I cleaned the text:</p>
<ul>
<li><p>Tokenized the resume</p>
</li>
<li><p>Removed bullets and punctuation</p>
</li>
<li><p>Stemmed words (important later)</p>
</li>
<li><p>Converted the resume into a structure like: <code>(index → sentence)</code></p>
</li>
</ul>
<p>This preserved <strong>order</strong>, which is critical for segmentation.</p>
<h2 id="heading-step-2-understanding-headings-the-painful-part">Step 2: Understanding headings (the painful part)</h2>
<p>This was the most time-consuming and frustrating part of the project — but also the most important.</p>
<p>Before going into the algorithm, you need to understand <strong>two dictionaries</strong>.</p>
<h3 id="heading-cue-phrases-multi-word-signals">Cue phrases (multi-word signals)</h3>
<p>These capture the different ways people name the same section.</p>
<p>Example: not everyone writes <em>Education</em>. Some write <em>Academic background</em>.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Example cue phrases (illustrative, not exhaustive)</span>
cue_phrases = {
    <span class="hljs-string">"academic background"</span>: <span class="hljs-string">"education"</span>,
    <span class="hljs-string">"professional experience"</span>: <span class="hljs-string">"experience"</span>
}
</code></pre>
<p>Think of this as:</p>
<p><em>“If I see this phrase, I know which section it really means.”</em></p>
<h3 id="heading-cue-words-single-word-signals">Cue words (single-word signals)</h3>
<p>These handle simple, clean headings like <code>Skills</code> or <code>Education</code>.</p>
<pre><code class="lang-python"><span class="hljs-comment"># Example cue words</span>
cue_words = {
    <span class="hljs-string">"education"</span>: <span class="hljs-string">"education"</span>,
    <span class="hljs-string">"experience"</span>: <span class="hljs-string">"experience"</span>
}
</code></pre>
<p>Important detail:</p>
<ul>
<li><p>These words are <strong>stemmed internally.</strong></p>
</li>
<li><p>So <code>education</code>, <code>educational</code>, and <code>educ</code> all collapse to the same signal.</p>
</li>
</ul>
<h2 id="heading-step-3-heading-detection-scan-1-and-scan-2">Step 3: Heading detection (Scan-1 and Scan-2)</h2>
<p>This is where most things broke before they worked.</p>
<h3 id="heading-scan-1-bigram-detection">Scan-1: Bigram detection</h3>
<ul>
<li><p>Create bigrams from each resume line</p>
</li>
<li><p>If a bigram matches a cue phrase:</p>
<ul>
<li><p>Store the <strong>section name</strong></p>
</li>
<li><p>Store the <strong>line index</strong></p>
</li>
<li><p>Add it to a <code>heading_document</code></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-scan-2-single-word-detection">Scan-2: Single-word detection</h3>
<ul>
<li><p>Look at short lines (likely headings)</p>
</li>
<li><p>If a token matches a cue word and isn’t already detected:</p>
<ul>
<li>Add it to the heading document</li>
</ul>
</li>
</ul>
<p>By the end of this step, I had:</p>
<pre><code class="lang-nginx">{ <span class="hljs-attribute">section_name</span> → starting_index }
</code></pre>
<p><strong>Important rule:</strong><br />Cue phrases, cue words, and resume text <strong>must be in the same processed format</strong>, or nothing works.</p>
<h2 id="heading-step-4-resume-segmentation">Step 4: Resume segmentation</h2>
<p>Once headings and their start indices are known:</p>
<ol>
<li><p>Sort them by index</p>
</li>
<li><p>Slice the resume between consecutive headings</p>
</li>
</ol>
<p>This gives clean sections like:</p>
<ul>
<li><p>summary</p>
</li>
<li><p>experience</p>
</li>
<li><p>projects</p>
</li>
<li><p>skills</p>
</li>
<li><p>education</p>
</li>
</ul>
<p>At this point, the resume is finally structured.</p>
<h2 id="heading-step-5-job-description-segmentation-semantic-approach">Step 5: Job Description segmentation (semantic approach)</h2>
<p>Instead of relying on JD headings, I took a different route.</p>
<p>I created <strong>semantic templates</strong>, like:</p>
<ul>
<li><p>“This section describes responsibilities”</p>
</li>
<li><p>“This section lists mandatory skills”</p>
</li>
</ul>
<p>Each JD sentence was embedded and compared against these templates, then bucketed into:</p>
<ul>
<li><p>role overview</p>
</li>
<li><p>responsibilities</p>
</li>
<li><p>required skills</p>
</li>
<li><p>nice to have</p>
</li>
</ul>
<p>This made the system robust to poorly formatted JDs.</p>
<h2 id="heading-step-6-matching-resume-jd">Step 6: Matching resume ↔ JD</h2>
<p>Each resume section and JD bucket was embedded.</p>
<p>Then I applied a <strong>controlled matching policy</strong>, for example:</p>
<ul>
<li><p>JD required skills → resume skills, experience, projects</p>
</li>
<li><p>JD responsibilities → resume experience, projects</p>
</li>
</ul>
<p>For each JD bucket, I took the <strong>maximum similarity</strong> across allowed resume sections.</p>
<p>Finally, I computed a <strong>weighted score</strong>, prioritizing required skills and responsibilities.</p>
<h2 id="heading-what-i-learned-this-matters-more-than-the-code">What I learned (this matters more than the code)</h2>
<ul>
<li><p>Always be clear about <strong>intermediate outputs</strong>, not just the final score</p>
</li>
<li><p>Don’t implement everything at once — build and validate step by step</p>
</li>
<li><p>Move forward <strong>only when the current step makes sense</strong></p>
</li>
<li><p>Before coding, write the logic on paper and explain it to yourself</p>
</li>
</ul>
<p>It sounds simple. It isn’t. But it works.</p>
<h2 id="heading-final-thoughts">Final thoughts</h2>
<p>If you’ve reached this point, you’re more than capable of building your own resume screening system.</p>
<p>If you’re still unsure, don’t worry — I’ll link:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/Deepika081/resume-screening-system">My GitHub repository</a></p>
</li>
<li><p><a target="_blank" href="https://onlinelibrary.wiley.com/doi/10.1155/2023/6044007">The research paper that inspired this approach</a></p>
</li>
</ul>
<p>Build it. Break it. Learn from it.<br />And when you’re done, come back and tell me what <em>you</em> learned.</p>
<p>Happy coding 🚀</p>
]]></content:encoded></item></channel></rss>