<?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" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Muanai Khalifah Revindo on Medium]]></title>
        <description><![CDATA[Stories by Muanai Khalifah Revindo on Medium]]></description>
        <link>https://medium.com/@muanaikhalifahr?source=rss-33bc6732a92e------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Stories by Muanai Khalifah Revindo on Medium</title>
            <link>https://medium.com/@muanaikhalifahr?source=rss-33bc6732a92e------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 23 May 2026 16:03:29 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@muanaikhalifahr/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[Taming Ambiguity in Indonesian Text Summarization: Building an IndoBART-v2 Pipeline from Research…]]></title>
            <link>https://medium.com/@muanaikhalifahr/taming-ambiguity-in-indonesian-text-summarization-building-an-indobart-v2-pipeline-from-research-f3be68ee6cac?source=rss-33bc6732a92e------2</link>
            <guid isPermaLink="false">https://medium.com/p/f3be68ee6cac</guid>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[indonesian-nlp]]></category>
            <category><![CDATA[nlp]]></category>
            <category><![CDATA[data-science]]></category>
            <dc:creator><![CDATA[Muanai Khalifah Revindo]]></dc:creator>
            <pubDate>Fri, 08 May 2026 12:55:12 GMT</pubDate>
            <atom:updated>2026-05-08T13:32:30.779Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Taming Ambiguity in Indonesian Text Summarization: Building an IndoBART-v2 Pipeline from Research to Engineering</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*jtm14wGvf2xmq5aV" /><figcaption>Photo by <a href="https://unsplash.com/@leiadakrozjhen?utm_source=medium&amp;utm_medium=referral">Leiada Krözjhen</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>In the rapidly evolving landscape of Indonesian Fintech and E-commerce, the primary challenge is not merely the volume of data. Instead, the real hurdle is the inherent ambiguity. Summarizing Indonesian news or financial reports is a complex task because semantic coherence often clashes with grammatical nuances. While high-performance models are abundant in the English NLP ecosystem, the Indonesian landscape is still maturing. This situation often requires engineers to work with specific, localized architectures that carry their own set of legacy baggage.</p><p>This project focuses on the implementation of an abstractive summarization pipeline using <strong>IndoBART-v2</strong> fine-tuned on the <strong>IndoSum</strong> dataset.</p><blockquote>However, moving this from a simple research script to a robust pipeline revealed a fundamental truth in Machine Learning Engineering: the model is often the easiest part.</blockquote><p>The real engineering happens in the friction. It involves navigating the “dependency hell” of outdated libraries, making calculated hardware tradeoffs between different GPU architectures, and designing a modular system that can scale beyond a single Jupyter Notebook. This article is not a simple report on achieving a high ROUGE score. It is a technical narrative of the engineering decisions, debugging struggles, and architectural choices required to build a production-ready NLP system in a localized context.</p><h3>Model Selection: Why IndoBART-v2?</h3><p>Choosing the right architecture for abstractive summarization is a critical decision that dictates the trajectory of the entire project. In the current era of Large Language Models (LLMs), the default inclination is often to reach for the largest decoder-only models available. However, an engineer must prioritize the alignment between the model objective and the specific task at hand.</p><h4>The Architecture Choice: Encoder-Decoder vs. Decoder-Only</h4><p>I chose <strong>IndoBART-v2</strong> specifically because of its encoder-decoder architecture. While decoder-only models like GPT excel at open-ended generation, they are not inherently optimized for sequence-to-sequence tasks like summarization.</p><p>The BART architecture utilizes a denoising autoencoder objective. It is trained by corrupting text and then forcing the model to reconstruct the original document. This pre-training objective is highly congruent with summarization. The encoder builds a robust representation of the full source context, while the decoder focuses on reconstructive synthesis. This results in summaries that are typically more grounded in the source text compared to the purely “predictive” nature of decoder-only architectures.</p><h4>Localized Pre-training vs. Multilingual Models</h4><p>Another major consideration was the trade-off between localized and multilingual pre-training. Massive models like mBART or T5 are trained on hundreds of languages. While impressive, they often suffer from “the curse of multilinguality,” where the model’s capacity is spread thin across diverse linguistic structures.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*xnDhjkvVSuOXlOK_.png" /><figcaption>Figure 1: Architectural Tradeoffs between BART and T5. Unlike T5 which focuses on predicting missing spans, BART’s objective of reconstructing the entire original sentence makes its encoder-decoder flow more robust for abstractive summarization, ensuring better groundedness in the resulting Indonesian text.</figcaption></figure><p>IndoBART-v2 is specifically pre-trained on a massive corpus of Indonesian text. This ensures that the model’s vocabulary and internal representations are finely tuned to the nuances of Indonesian formal and informal registers. For a domain like Fintech, where specific legal and financial terminology is common, a localized vocabulary is more efficient than a generalized multilingual one.</p><h4>Engineering Constraints: Compute and Latency</h4><p>Finally, engineering is the art of working within constraints. Fine-tuning a massive LLM requires a level of compute resources that is often not justifiable for specific niche tasks. IndoBART-v2 provides a “sweet spot” in terms of parameter count. It is large enough to capture complex semantic relationships but small enough to be fine-tuned on a single consumer-grade or mid-tier enterprise GPU.</p><p>This efficiency translates directly to production benefits. A smaller, specialized model offers lower inference latency and reduced operational costs. In a high-traffic E-commerce environment, being able to serve summaries in milliseconds is far more valuable than having a slightly more “creative” but prohibitively slow model.</p><h3>The Debugging Chronicles: Solving Legacy Hell</h3><p>In the idealized world of tutorials, libraries work together in perfect harmony. In professional machine learning engineering, however, you often find yourself caught in the crossfire of version mismatches and abandoned repositories. This project was no exception. The conflict centered on the indobenchmark toolkit, a vital but aging library, and the modern transformers ecosystem.</p><h4>The Conflict: A Tale of Two Versions</h4><p>The indobenchmark library is a snapshot of the Indonesian NLP state-of-the-art from circa 2021. Since then, the Hugging Face transformers library has undergone significant structural changes. When I attempted to initialize the IndoNLGTokenizer, the system immediately collapsed with a series of ImportError and AttributeError messages.</p><p>The root cause was technical debt in the dependency chain. The legacy tokenizer attempted to access private internal members of the transformers.utils module, specifically flags like is_tf_available and _is_jax. In newer versions of transformers, these members had been moved, renamed, or deleted. This created a complete blockade: I needed the specific logic of the Indonesian tokenizer, but the environment it required no longer existed.</p><h4>The Solution: Monkey Patching and Runtime Surgery</h4><p>Faced with a broken third-party library, an engineer has two choices: downgrade the entire environment to a vulnerable, outdated state, or perform “runtime surgery.” I chose the latter through a technique known as <strong>Monkey Patching</strong>.</p><p>Monkey patching allows a developer to modify or extend the behavior of a library at runtime without altering the actual source code on disk. This was achieved in two strategic phases:</p><ol><li><strong>Namespace Restoration</strong>: I manually injected the missing flags back into the transformers utility modules before the tokenizer was imported. By faking the existence of _is_jax and is_tf_available, I satisfied the legacy library&#39;s requirements without actually needing those frameworks present.</li><li><strong>Method Wrapping</strong>: The modern Trainer class passes several new arguments to the tokenizer&#39;s pad and decode methods that the legacy IndoNLGTokenizer does not recognize. To solve this, I wrapped the original methods in custom functions that &quot;swallow&quot; unknown arguments. This ensured that when the training loop called for padding, the outdated tokenizer would only receive the parameters it knew how to handle.</li></ol><pre>import transformers.utils.generic<br>import types<br><br>missing_flags = [&quot;_is_jax&quot;, &quot;_is_numpy&quot;, &quot;_is_tensorflow&quot;, &quot;_is_torch&quot;, &quot;_is_torch_device&quot;]<br>for flag in missing_flags:<br>    if not hasattr(transformers.utils.generic, flag):<br>        setattr(transformers.utils.generic, flag, False)<br><br>def apply_tokenizer_patches(tokenizer):<br>    _original_pad = tokenizer.pad<br><br>    def custom_pad(*args, **kwargs):<br>        kwargs.pop(&#39;padding_side&#39;, None)<br>        return _original_pad(*args, **kwargs)<br><br>    tokenizer.pad = custom_pad<br><br>    def custom_save_vocabulary(self, save_directory, filename_prefix=None):<br>        return ()<br><br>    tokenizer.save_vocabulary = types.MethodType(custom_save_vocabulary, tokenizer)<br><br>    _original_decode = tokenizer.decode<br><br>    def custom_decode(self, token_ids, skip_special_tokens=False, clean_up_tokenization_spaces=None, **kwargs):<br>        return _original_decode(token_ids, skip_special_tokens=skip_special_tokens, **kwargs)<br><br>    tokenizer.decode = types.MethodType(custom_decode, tokenizer)<br><br>    return tokenizer</pre><h4>A Note on Technical Debt: The Temporary Nature of Patching</h4><p>It is important to acknowledge that while monkey patching rescued the pipeline, it is not a permanent architectural fix. Runtime patches are inherently brittle. If the transformers library undergoes another major update or if indobenchmark is eventually modernized, this patch could become redundant or even cause new conflicts. In a long-term production environment, the strategic move would be to fork the legacy library and update its source code to maintain compatibility. However, in the context of rapid prototyping and research, this tactical intervention allowed us to move forward without being paralyzed by a fractured ecosystem.</p><h4>Key Takeaway: Engineering Maturity over Tool Dependency</h4><p>Solving this “Legacy Hell” was perhaps the most informative part of the project. It shifted the focus from being a mere user of tools to being an investigator of those tools.</p><p>This level of debugging demonstrates a crucial aspect of software engineering maturity. In a production environment, you cannot always wait for a maintainer to update a library that has been stale for years.</p><blockquote>The ability to trace a stack trace back to a specific line of a third-party dependency, understand the breaking change in the upstream library, and implement a non-destructive fix at runtime is a vital skill. It is the difference between a project that stalls at the first error and one that reaches deployment despite a fractured ecosystem.</blockquote><h3>Hardware Realism: T4 vs. P100 Tradeoffs</h3><p>In the world of cloud computing and Kaggle environments, VRAM capacity is often used as the primary metric for GPU selection. A common assumption is that a card with high memory capacity is objectively superior for training large models.</p><blockquote>However, this project served as a reminder that raw memory is a hollow metric if the underlying architecture cannot process data efficiently.</blockquote><h4>The Choice: Architecture over Capacity</h4><p>When selecting a GPU for fine-tuning IndoBART, I was faced with a choice between the NVIDIA P100 (Pascal architecture) and the NVIDIA T4 (Turing architecture). On paper, the P100 is a powerhouse with high memory bandwidth. Yet, for modern transformer workloads, the T4 is often the more pragmatic choice.</p><p>The decision was not about the quantity of VRAM. It was about the generational leap in specialized instruction sets. While the P100 is an excellent general-purpose accelerator, it lacks the specialized hardware features that modern deep learning libraries are optimized to exploit.</p><h4>Deep Dive: Tensor Cores and Mixed Precision</h4><p>The defining factor in this hardware tradeoff was the presence of <strong>Tensor Cores</strong> in the T4. Unlike the older Pascal architecture, Turing was designed with these specialized cores to accelerate matrix multiplication, which is the fundamental operation in transformer layers.</p><p>By utilizing Tensor Cores, I could implement <strong>Mixed Precision Training (FP16)</strong>. This technique allows the model to perform most calculations in 16-bit half-precision while maintaining critical weights in 32-bit to preserve accuracy. On the T4, FP16 is not just a memory-saving trick; it is a hardware-accelerated feature that provides a massive boost in computational speed. The P100, while capable of 16-bit operations, does not possess the dedicated Tensor Cores to turn that precision reduction into a significant performance gain.</p><pre>training_args = Seq2SeqTrainingArguments(<br>    output_dir=&quot;./indosum-bart-summarization&quot;,<br>    per_device_train_batch_size=4,<br>    gradient_accumulation_steps=4,<br>    learning_rate=2e-5,<br>    weight_decay=0.01,<br>    num_train_epochs=20,<br>    predict_with_generate=True,<br>    fp16=True, # Crucial for T4 Tensor Core utilization<br>    evaluation_strategy=&quot;epoch&quot;,<br>    save_strategy=&quot;epoch&quot;,<br>    load_best_model_at_end=True<br>)</pre><h4>The Impact: Throughput and Training Stability</h4><p>This architectural choice had a direct impact on the training lifecycle. By opting for the T4 and enabling FP16, I achieved significantly higher throughput, measured in samples processed per second.</p><p>Furthermore, the T4 handled the stability of mixed precision training more gracefully. Modern training loops use dynamic loss scaling to prevent numerical underflow in FP16. The Turing architecture is better aligned with these software optimizations, resulting in a training curve that was both faster and less prone to sudden gradient explosions.</p><p>Ultimately, this tradeoff proves a vital engineering lesson:</p><blockquote>Always align your hardware choice with your software’s optimization capabilities. For fine-tuning transformers, the efficiency of specialized cores often outweighs the brute force of raw memory bandwidth.</blockquote><h3>Architecture: Designing for Modularity</h3><p>A common pitfall in machine learning projects is the “monolithic notebook” approach. While packing every line of code into a single, massive script is convenient for a quick experiment, it is a nightmare for long-term scalability and maintenance. For this project, I chose to dismantle the monolithic structure in favor of a modular architecture built on the principle of separation of concerns.</p><h4>The Monolith vs. The Modular Approach</h4><p>In a production-ready environment, the logic for cleaning data should not live in the same space as the logic for updating model weights. By decoupling these processes, we ensure that a bug in the data pipeline does not silently corrupt the training loop. This modularity also allows different team members to work on separate parts of the system simultaneously without causing merge conflicts or logic overlaps.</p><h4>The Structural Components</h4><p>I reorganized the codebase into three distinct functional managers, each with a clear and isolated responsibility:</p><ul><li><strong>IndoSumManager</strong>: This is the “Source of Truth” for the data. It handles the raw file orchestration, flattening nested JSON structures, and executing the heavy lifting of text cleaning. By isolating these tasks, I can swap out the dataset or modify cleaning rules without ever touching the model configuration.</li><li><strong>DataPreprocessor</strong>: This component manages the bridge between raw text and tensors. It handles the deterministic splitting of data using a fixed random_state=42 to ensure reproducibility. It also encapsulates the tokenization logic, ensuring that max length constraints and padding strategies remain consistent across training and evaluation.</li><li><strong>SummarizationTrainer</strong>: This is the engine room. It wraps the Hugging Face Trainer API, the DataCollator, and the EarlyStopping callbacks. Its only job is to take processed datasets and an initialized model, then execute the training lifecycle according to the specified hyperparameters.</li></ul><h4>Benefits: Scalability and Maintainability</h4><p>The primary benefit of this design is <strong>Scalability</strong>. If I decide to move from news summarization to summarizing customer reviews in an E-commerce context, I only need to implement a new data manager. The preprocessing and training modules remain largely untouched.</p><p>Furthermore, this structure enhances <strong>Maintainability</strong>. If a specific error occurs during tokenization, I know exactly which class to investigate. There is no need to sift through a thousand lines of code to find the offending loop. This level of organization reflects a professional software engineering mindset: code is written not just for the machine to execute, but for other engineers to read, debug, and improve.</p><h3>Evaluation: A Skeptical Perspective</h3><p>In machine learning, it is easy to become intoxicated by rising metrics. However, a professional engineer must maintain a healthy skepticism toward the results. While the ROUGE scores provide a mathematical baseline for performance, they often mask the underlying pathologies of the model.</p><h4>The Quantitative Reality: Observing the Overfitting Trap</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*o_vkwPEV7HfozAk4kcwDGg.png" /><figcaption>Figure 2: Visualizing the Overfitting Divergence. While the Training Loss decays sharply, the Validation Loss begins to climb steadily after Epoch 3. This divergence is a classic signal that the model is shifting from semantic learning to pattern memorization, marking the optimal point for early stopping.</figcaption></figure><p>The training logs reveal a classic divergence in the optimization process. By the twentieth epoch, the training loss dropped significantly from 0.53 to 0.04. However, the validation loss began to creep upward as early as the third epoch, eventually reaching 0.66.</p><p>This is a clear signal of overfitting. The model shifted from learning generalized semantic structures to memorizing the specific patterns of the training data. While the ROUGE-1 scores plateaued at approximately 0.35, the rising validation loss indicates that the model was losing its ability to generalize to unseen data. In a production environment, this suggests that training should have been halted much earlier, likely around epoch three, to preserve the model’s robustness.</p><h4>The Qualitative Gap: Hallucinations and Generation Noise</h4><p>Looking at the sample evaluation provides even deeper insights than the loss curves. In the provided example, the model successfully captures the core entities, such as “Satlak Prima” and the “Rp 500 miliar” budget. However, as the generation continues, we see the emergence of “noise.”</p><p>The AI summary ends with a series of repetitive dots and fragmented sentences regarding budget proposals and DPR decisions. This is a common failure mode in sequence-to-sequence models known as the “End-of-Sequence” (EOS) problem or repetitive looping. Even though the early part of the summary is highly accurate, the messy termination would make the output unsuitable for a direct user-facing interface without further post-processing or better penalty constraints during inference.</p><h4>Factual Consistency vs. Token Matching</h4><p>The most critical observation from the sample is the model’s reliance on extraction. The AI summary mimics the source text closely, which explains the decent ROUGE scores. Yet, it also introduced specific details about a “90 miliar” additional fund. While this might be factually present in the full source text, the model struggled to synthesize this information into a coherent conclusion, instead trailing off into repetitive punctuation.</p><p>This highlights the limitation of ROUGE. The metric rewards the model for matching tokens, but it does not penalize it for losing structural integrity at the end of a paragraph.</p><blockquote>As engineers, we must realize that a high ROUGE score does not necessarily equate to a high-quality summary. True success is defined by a model that knows when to stop talking and how to maintain a consistent logical flow until the very last token.</blockquote><h3>Conclusion: Moving Toward Production</h3><p>Building an Indonesian text summarization pipeline is a masterclass in managing technical tradeoffs. This project moved beyond the theoretical allure of model training and into the gritty reality of systems engineering. From performing runtime surgery on legacy libraries to making calculated hardware choices, the journey emphasized that a successful machine learning project is built on the foundation of robust software principles.</p><h4>Lessons from the Training Trenches</h4><p>The most immediate takeaway from this project is the necessity of rigorous monitoring. The divergence between training and validation loss serves as a textbook reminder that “more training” does not equal “better performance.” In future iterations, implementing more aggressive early stopping criteria would be a priority. By halting the training as soon as the validation loss begins its upward trend, we can preserve the model’s ability to generalize and prevent it from becoming a mere memorization engine.</p><h4>The Road to Deployment: Optimization and Serving</h4><p>To transition this model from a Kaggle notebook to a production environment, several optimization steps are required. The current raw model, while performant, has a footprint that could be improved for real-time serving.</p><ol><li><strong>Quantization</strong>: Moving the model from FP16 to INT8 or even 4-bit quantization would significantly reduce the memory footprint and increase inference speed. This is essential for deploying the model in cost-sensitive environments or on edge devices.</li><li><strong>Inference Serving</strong>: Implementing the model through a high-performance framework like FastAPI or NVIDIA Triton would allow us to handle concurrent requests and manage the lifecycle of the model more efficiently than a standard Python script.</li><li><strong>Advanced Fine-Tuning</strong>: For future versions, exploring Parameter-Efficient Fine-Tuning (PEFT) techniques like LoRA (Low-Rank Adaptation) could provide a more efficient path to specialization. This would allow us to adapt the model to specific sub-domains, such as legal contracts or customer support chats, without the need for full parameter updates.</li></ol><h4>Final Thoughts: The Engineering Mindset</h4><p>Ultimately, this project confirms that the role of an ML Engineer is to be a bridge between research and reality. It is not enough to have a model that produces a high ROUGE score; the model must be maintainable, the code must be modular, and the deployment path must be clear.</p><p>As we continue to push the boundaries of localized NLP for the Indonesian market, our focus must remain on building systems that are not just “intelligent,” but also predictable and production-ready. The goal is to move from experimental prototypes to real-world systems that can reliably distill information in the high-stakes environments of Fintech and E-commerce.</p><h4>The GitHub Repository</h4><p>You can find the complete source code, the modular managers, and the training notebooks in my GitHub repository. I encourage you to fork the project, experiment with different hyperparameters, or suggest improvements to the data pipeline.</p><p><a href="https://github.com/Muanai/indobart-indosum-summarizer">GitHub - Muanai/indobart-indosum-summarizer</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f3be68ee6cac" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Street Images to Geo-Spatial Insights: Detecting Telecom Infrastructure with YOLOv8]]></title>
            <link>https://medium.com/@muanaikhalifahr/from-street-images-to-geo-spatial-insights-detecting-telecom-infrastructure-with-yolov8-5fb419dbf4f4?source=rss-33bc6732a92e------2</link>
            <guid isPermaLink="false">https://medium.com/p/5fb419dbf4f4</guid>
            <category><![CDATA[yolov8]]></category>
            <category><![CDATA[telecommunication]]></category>
            <category><![CDATA[computer-vision]]></category>
            <category><![CDATA[object-detection]]></category>
            <category><![CDATA[geospatial-analytics]]></category>
            <dc:creator><![CDATA[Muanai Khalifah Revindo]]></dc:creator>
            <pubDate>Mon, 16 Mar 2026 09:44:23 GMT</pubDate>
            <atom:updated>2026-03-16T09:44:23.793Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dfjCo2I5CxQIODsU" /><figcaption>Photo by <a href="https://unsplash.com/@denheld?utm_source=medium&amp;utm_medium=referral">Diana den Held</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Telecom infrastructure is the silent backbone of our digital lives, woven into the chaotic fabric of our city streets. Yet for business analysts, a simple question remains surprisingly difficult to answer: <strong>who owns which pole?</strong></p><p>Fixed broadband providers often mark their infrastructure using distinct colored bands attached to utility poles. In theory, these markers should make asset identification straightforward. In practice, mapping them at scale still relies heavily on manual field surveys — a process that is slow, costly, and difficult to scale.</p><p>During my internship at Telkomsel’s Business Growth &amp; Analytics division, I explored whether computer vision could automate this process. The goal was to transform raw street-level imagery into structured insights about infrastructure ownership.</p><p>This project evolved beyond simply training an object detection model. Instead, I engineered an end-to-end pipeline combining <strong>YOLOv8-based detection</strong> with a custom geo-spatial workflow that maps detected assets directly to administrative boundaries.</p><p>In this article, I will walk through the technical journey from a baseline model to a functional MVP, including:</p><ul><li><strong>The Data-Centric Pivot: W</strong>hy resolution and stratified dataset design mattered more than model tuning.</li><li><strong>Handling Real-World Constraints: U</strong>sing physics-aware augmentation and synthetic data to handle noisy street environments.</li><li><strong>From Pixels to Maps: I</strong>ntegrating EXIF GPS extraction and reverse geocoding to generate regional infrastructure reports.</li></ul><h4>The Problem: Beyond the Clean Benchmark</h4><p>In a controlled environment, object detection is a solved problem. But the streets of Indonesia are anything but controlled. This project was about the small, often weathered <strong>color bands</strong> wrapped around them that signify ownership. Moving from a sandbox to a real-world <strong>pilot-ready prototype</strong> meant facing a “Python Trap” of sorts: the logic was sound, but the environment was hostile.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VLrpro59lIIayEiDpmw3Mg.jpeg" /><figcaption><strong>Figure 1:</strong> Raw field capture showcasing typical urban constraints: complex backgrounds and weathered infrastructure markers that define the provider’s visual identity.</figcaption></figure><p>We identified three primary bottlenecks that made standard “off-the-shelf” training architecturally non-scalable:</p><ul><li><strong>The Resolution Trap (Thin Features):</strong> These color bands are thin and often placed high on utility poles. At standard 640px resolutions, the spatial features of these bands — especially when surrounded by thin fiber optic cables — suffer from severe aliasing, effectively disappearing into a mess of pixels.</li><li><strong>Environmental Noise &amp; Occlusion:</strong> Indonesian street scenes are dense. Our target markers are frequently masked by tangled “spaghetti” cables, tropical foliage, or roadside banners. The model doesn’t just need to see the band; it needs to distinguish it from a sea of visual clutter.</li><li><strong>The Long-Tail Distribution:</strong> Infrastructure isn’t distributed equally. Some providers have a massive presence, while others — like Lintasarta — are so rare in the field that they became “ghost classes” during initial training, leading to a catastrophic <strong>0.086 mAP</strong> before intervention.</li></ul><blockquote>We realized that achieving reliability required more than just more training time; it required a <strong>data-centric overhaul</strong> specifically tuned to these small-scale markers.</blockquote><h4>Dataset &amp; Detection: Decoding the Provider “DNA”</h4><p>The dataset was not just collected; it was engineered for <strong>visual identity classification</strong>. To ensure the highest data integrity for a <strong>pilot-ready prototype</strong>, I personally captured 399 raw images of street-level infrastructure.</p><p>However, quantity does not always equal quality in computer vision. I manually filtered this raw pool down to a curated set of <strong>high-quality images</strong>, selecting only those where the provider-specific color bands were clearly identifiable despite real-world obstructions.</p><p>These bands act as the visual “DNA” for operators like <strong>CBN, Indosat, MyRepublic, and Lintasarta</strong>. However, our “ground truth” was plagued by real-world inconsistency:</p><ul><li><strong>Weathering &amp; Chromatic Shift:</strong> A red band on a brand-new pole is vivid. A red band after years of tropical sun and rain is faded or rusted. We had to implement aggressive saturation variance (hsv_s=0.7) to ensure the model learned the <em>identity</em> of the color, not just its perfect hex code.</li><li><strong>Strategic Data Curated:</strong> From a raw pool of 399 field captures, only 157 high-quality images were selected to ensure that every pixel the model learned from was a valid representation of real infrastructure.</li></ul><p>By shifting our focus from the pole to the <strong>color band as a high-frequency feature</strong>, we moved toward a <strong>1248px resolution strategy</strong> — a first-class hyperparameter decision that finally allowed the model to “see” the markers clearly.</p><h4>Model &amp; Training Strategy: Engineering Reliable Learning</h4><p>Building the model wasn’t just about picking an architecture; it was about ensuring the architecture could actually learn from the noise. We chose <strong>YOLOv8</strong> for its balance of speed and accuracy, but achieving a <strong>pilot-ready status</strong> required several strategic interventions beyond the default configuration:</p><ul><li><strong>Stratified Splitting:</strong> Random splits are a luxury of balanced datasets. To prevent the scenario where all rare provider instances (like Lintasarta) ended up in the validation set — leaving the model with nothing to learn — we implemented a <strong>stratified split strategy</strong> to maintain class distribution across both sets.</li><li><strong>Physics-Aware Augmentation:</strong> Modern augmentation can be too aggressive. We locked our transformations to real-world logic — specifically disabling vertical flips (flipud=0.0) because infrastructure objects never defy gravity. We also utilized occlusion simulation (erasing=0.4) to mimic the chaotic &quot;spaghetti&quot; of Indonesian street scenes.</li><li><strong>Synthetic Data Injection:</strong> To address representation collapse in minority classes, we injected 12 <strong>AI-generated synthetic samples</strong> exclusively into the training set. Crucially, the validation set remained <strong>100% real-world data</strong>, ensuring that our mAP scores reflected genuine generalization rather than “synthetic-assisted” evaluation.</li></ul><h4>The Small Object Problem: Solving Feature Collapse</h4><p>In infrastructure detection, scale is the ultimate enemy. At the standard <strong>640px</strong> resolution, thin assets — specifically provider color bands — often collapse into a few ambiguous pixels, making them invisible to the model.</p><p>To counter this, we treated <strong>resolution as a first-class hyperparameter</strong>:</p><ul><li><strong>The 1248px Shift:</strong> We doubled the training resolution to <strong>1248px</strong> to recover fine-grained spatial features lost during downsampling.</li><li><strong>The Impact:</strong> This was the primary driver for our “redemption arc.” For the Lintasarta class, this high-resolution strategy helped push mAP50 from a near-zero <strong>0.086</strong> to <strong>0.497</strong>.</li></ul><p>By prioritizing data density over model complexity, we moved from mere object detection to <strong>feature recovery</strong>.</p><h4>Experiment Results: The Data-Centric Redemption</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KI270EFO7-IShkckK6Vb1A.png" /><figcaption><strong>Figure 2:</strong> The iterative evolution of model performance. Each jump in mAP50 represents a specific bottleneck resolution: from the initial standardization in v1.0 to the critical v2.1, driven by high-resolution feature recovery and synthetic data injection.</figcaption></figure><p>After multiple iterations of dataset engineering and surgical model tuning, the final candidate (v2.1) achieved an overall <strong>mAP50 of 0.763</strong> on the validation set. While the metric itself is solid for a chaotic street-level task, the real story lies in the “redemption arc” of our minority classes.</p><p>By implementing stratified training and high-resolution feature recovery, we saw a dramatic shift in model awareness:</p><ul><li><strong>Indosat &amp; CBN:</strong> Maintained “God Tier” performance with mAP50 scores consistently above <strong>0.90</strong>, proving the model’s absolute grasp of their visual markers.</li><li><strong>The Minority Success:</strong> Our previously “invisible” class, <strong>Lintasarta</strong>, climbed from a catastrophic 0.086 to a functional <strong>0.497 mAP50</strong>.</li><li><strong>Robustness:</strong> The model demonstrated increased resilience against visual noise such as trees and banners, a direct result of our physics-aware augmentation and stratified validation strategy.</li></ul><p>These results validate a core lesson of the project:</p><blockquote>In real-world applications, an unreliable validation set is more damaging than an underperforming model. By keeping our validation <strong>100% real-world</strong>, we ensured these metrics translate to actual field reliability.</blockquote><h4>Geo-Spatial Integration: From Pixels to Regional Intelligence</h4><p>The true “Unique Selling Point” of this pipeline is its ability to move beyond simple bounding boxes. A detection on a screen has little value to a business analyst unless it is grounded in <strong>spatial context</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LDRWlGgRLah9rXkQetjNzA.png" /><figcaption><strong>Figure 3:</strong> The standalone inference engine in action. The tool extracts GPS metadata and performs offline reverse-geocoding to map detections directly to administrative sub-districts (Kecamatan).</figcaption></figure><p>To bridge this gap, the pipeline integrates a sophisticated geo-spatial module that transforms raw detections into structured business intelligence:</p><ul><li><strong>Automated Metadata Extraction:</strong> The engine automatically parses <strong>EXIF metadata</strong> from the source imagery to extract precise GPS coordinates (Latitude/Longitude) at the moment of capture.</li><li><strong>Offline Reverse Geocoding:</strong> Using <strong>BPS (Statistics Indonesia) Shapefiles</strong>, the system performs offline reverse geocoding to map these coordinates to specific administrative boundaries, such as <strong>Kecamatan (Sub-districts)</strong>.</li><li><strong>Structured Reporting:</strong> Instead of just annotated images, the final output is a consolidated <strong>CSV report</strong>. This allows infrastructure presence to be aggregated and analyzed at a regional level — enabling the Business Growth team to identify competitor expansion trends with a single click.</li></ul><p>By integrating geo-analytics directly into the inference engine, we transformed a computer vision experiment into a functional <strong>Geospatial Business Intelligence</strong> tool.</p><h4>Lessons Learned: Engineering Over Intuition</h4><p>The journey from a baseline model to a <strong>pilot-ready prototype</strong> provided several critical insights into the behavior of applied Machine Learning in uncontrolled environments. Beyond the metrics, these are the engineering principles that defined the project’s success:</p><ul><li><strong>Data Dominates Architecture:</strong> Early iterations proved that modern detection architectures (YOLOv8) can handle dominant classes with ease, but consistently fail on minority ones. Meaningful improvement only occurred after intervening at the dataset level (stratification and synthetic injection), confirming that model capacity cannot compensate for poor data balance.</li><li><strong>Augmentation Without Realism is Noise:</strong> Unconstrained transformations, such as vertical flips, risked violating real-world physics. Restricting our strategy to physically plausible transformations resulted in more stable learning and reduced spurious detections.</li><li><strong>Resolution is a First-Class Hyperparameter:</strong> For high-frequency features like fiber cables and small markers, resolution choice has a larger impact than most optimizer-level tweaks. Increasing image size to <strong>1248px</strong> was the only way to recover features that were otherwise lost to downsampling.</li><li><strong>Synthetic Data as a Signal Amplifier:</strong> Injecting synthetic samples enabled the model to learn representations for underrepresented classes. However, we maintained a strict “zero-contamination” policy — synthetic data is a training tool, but validation must always remain 100% real-world to ensure statistical integrity.</li><li><strong>Metrics Require Context:</strong> High mAP scores can be deceptive if the sample size is small. We learned to interpret performance in the context of sample size rather than as standalone indicators, especially for classes with fragile statistics.</li></ul><h4>Conclusion: Transforming Pixels into Intelligence</h4><p>This project demonstrates that the value of Computer Vision in a business context lies far beyond simple bounding boxes. By engineering a pipeline that respects real-world constraints — from the chaos of Indonesian streets to the nuances of class imbalance — we moved from a technical experiment to a functional <strong>Geospatial Business Intelligence</strong> tool.</p><p>By combining high-resolution detection with automated spatial data integration, we proved that raw street imagery can be transformed into actionable infrastructure insights at the regional level. While this prototype is just the beginning, it serves as a pragmatic blueprint for how AI can support competitive landscape analysis and infrastructure expansion in the telecommunications industry.</p><p>The full technical implementation of this project — including the modular training scripts, the stratified dataset engineering logic, and the geo-spatial inference engine — is available on GitHub.</p><p>I have documented every failure and breakthrough in the <strong>Experiment Log</strong>, providing a transparent look at how we moved from a baseline of 0.522 to a functional 0.763 mAP50.</p><p>I invite you to explore the repository, fork the code, and test the limits of the <strong>1248px resolution strategy</strong>. Whether it’s optimizing the inference speed of the geo-spatial module or refining the synthetic data injection for minority classes, there is always room to squeeze out more performance from a real-world pipeline.</p><p><strong>Check out the repo here:</strong></p><p><a href="https://github.com/Muanai/telecom-infrastructure-object-detection">GitHub - Muanai/telecom-infrastructure-object-detection: End-to-end object detection project for telecom infrastructure using real-world field data with challenging conditions such as rust, occlusion, and adverse weather</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5fb419dbf4f4" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Credit Risk Feature Engineering with Python & Numba]]></title>
            <link>https://medium.com/@muanaikhalifahr/credit-risk-feature-engineering-with-python-numba-eb9643908f9c?source=rss-33bc6732a92e------2</link>
            <guid isPermaLink="false">https://medium.com/p/eb9643908f9c</guid>
            <category><![CDATA[machine-learning-python]]></category>
            <category><![CDATA[credit-risk-modelling]]></category>
            <category><![CDATA[feature-engineering]]></category>
            <dc:creator><![CDATA[Muanai Khalifah Revindo]]></dc:creator>
            <pubDate>Fri, 19 Dec 2025 08:50:15 GMT</pubDate>
            <atom:updated>2026-01-24T11:52:57.584Z</atom:updated>
            <content:encoded><![CDATA[<h3>50,000x Faster: Engineering “Stateful” Credit Risk Features with Python &amp; Numba</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*j9sIs9L4PPBRf3Jp" /><figcaption>Photo by <a href="https://unsplash.com/@jonasleupe?utm_source=medium&amp;utm_medium=referral">Jonas Leupe</a> on <a href="https://unsplash.com?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Processing behavioral data for 1 million users shouldn’t bring your pipeline to a halt. Yet, when I tried to capture complex delinquency streaks using standard Python loops, the estimated runtime was abysmal. This article documents how I rewrote the core engine using Numba to achieve a 50,000x speedup and a +4.2% AUC uplift — proving that high-performance engineering is essential for modern credit risk modeling.</p><h4>The Problem: The “Static Feature” Blindspot</h4><p>Most credit risk models share a fatal flaw: they rely too heavily on <strong>static aggregates</strong>. We traditionally condense months of transactional history into simple metrics like mean_bill_amount, total_payments, or max_utilization. While these features are easy to compute using standard SQL or Pandas group-by operations, they flatten the dimension of time, effectively blinding the model to behavioral trends.</p><p>Consider two hypothetical users, both with an average credit utilization of 50%:</p><ul><li><strong>User A (The Disciplined Repayer):</strong> Started with high debt (85%) but has consistently paid it down every single month, ending at a healthy 15%. <strong>(Risk: Decreasing)</strong></li><li><strong>User B (The Volatile Spender):</strong> Started low (15%), but exhibited erratic behavior — spiking to 65%, briefly recovering, then relapsing to a dangerous 85%. <strong>(Risk: High &amp; Unstable)</strong></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yyVK5WS22_67CB_1ApAJ_Q.png" /><figcaption>Figure 1: Two distinct user profiles. User A [85, 70, 60, 45, 25, 15], while User B [15, 25, 65, 40, 70, 85].</figcaption></figure><blockquote>To a model fed only static aggregates, these two users look identical. To catch the difference, we need <strong>stateful, temporal features</strong> — metrics that measure velocity, acceleration, and consecutive streaks (e.g., <em>“How many months in a row has this user paid late?”</em>).</blockquote><p><strong>The Engineering Bottleneck</strong> Here lies the friction: calculating these stateful features is computationally expensive.</p><ul><li><strong>Standard Python Loops:</strong> Iterating through millions of rows to check conditions for every user is agonizingly slow (as shown in the benchmarks below).</li><li><strong>Vectorized Pandas:</strong> While fast for simple math, Pandas struggles with complex conditional logic that depends on the <em>previous</em> row’s state (like resetting a counter when a payment is made).</li></ul><p>We are often forced to choose: build a “dumb” model with fast simple features, or build a “smart” model with a pipeline that takes hours to run. This project aims to break that trade-off.</p><h4>The Solution: Numba Powered Feature Engine</h4><p>To break the trade-off between model complexity and pipeline latency, I turned to <strong>Numba</strong>, a Just-In-Time (JIT) compiler for Python.</p><p>Unlike standard Python which interprets code line-by-line (adding significant overhead to loops), Numba translates a subset of Python and NumPy code into fast machine code using the LLVM compiler library. This allows us to write complex, stateful logic in pure Python syntax — specifically for loops that track user history—while executing them at C-like speeds.</p><p><strong>The Strategy: User-Time Matrix</strong> The architecture is straightforward. First, I pivot the raw transaction logs into a dense <strong>User × Time</strong> matrix using NumPy. This data structure is cache-friendly and ideal for Numba to iterate over. Once structured, we apply our custom feature logic decorated with @njit, effectively bypassing the Python Global Interpreter Lock (GIL) for the heavy lifting.</p><p><strong>Benchmarking: The 50,000x Speedup</strong> I tested three implementations to measure the execution time for calculating <strong>rolling window statistics (e.g., 3-month moving average)</strong>. Even for this standard operation, moving from Python loops to Numba resulted in a massive speedup. The results were not just an improvement; they were a paradigm shift.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zk43vDSMex9k4QyNGjvX3A.png" /><figcaption>Figure 2: Visualizing the bottleneck removal: From 55 seconds to 0.0011 seconds using Numba JIT.</figcaption></figure><p><strong>Why This Matters</strong> A runtime of 58 seconds might seem manageable for a small dataset, but in a production environment processing millions of users, that latency is unacceptable. Numba bringing the compute time down to <strong>1 millisecond</strong> means we are no longer constrained by cost or time. We can now afford to experiment with dozens of complex, multi-window behavioral features without stalling the entire data pipeline.</p><h4>Engineering Behavioral Signals</h4><p>With the computational constraints removed, we can move beyond simple sums and averages. I designed three flagship features specifically to capture the “story” of a user’s financial health over time. These features are <strong>stateful</strong> — meaning the value at month <em>t </em>depends on what happened at month <em>t</em>-1— which is exactly where Numba shines.</p><h4>1. Consecutive Late Payment Streak</h4><blockquote>The Question: Is the user forgetting to pay, or are they insolvent?</blockquote><p>A standard aggregation might tell you a user was late 3 times in a year. But there is a massive risk difference between a user who misses a payment every four months (random forgetfulness) and a user who misses payments for three months <strong>in a row</strong> (financial distress).</p><ul><li><strong>The Logic:</strong> I implemented a loop that increments a counter for every consecutive month a payment is missed (days_late &gt; 0) and resets it to zero immediately upon a valid payment.</li><li><strong>The Signal:</strong> This feature isolates <strong>systematic delinquency</strong> from stochastic errors. A high streak is one of the strongest predictors of an impending default.</li></ul><h4>2. Payment-to-Bill Velocity (Trend Slope)</h4><blockquote>The Question: Is the user’s ability to pay deteriorating?</blockquote><p>Most models look at the <em>current</em> Payment-to-Bill ratio. I wanted to see the <strong>trajectory</strong>.</p><ul><li><strong>The Logic:</strong> This feature calculates the linear slope (gradient) of the user’s Payment-to-Bill ratio over a 6-month window.</li><li>A <strong>negative slope</strong> means the gap between what they owe and what they pay is widening (Danger).</li><li>A <strong>positive slope</strong> indicates recovery (Safe).</li><li><strong>The Signal:</strong> This acts as an <strong>early warning system</strong>. A user might still be paying above the minimum threshold today, but a sharp negative velocity indicates they will likely breach it soon. Standard SQL struggles to calculate per-user regression slopes efficiently; Numba handles it effortlessly.</li></ul><h4>3. Critical Balance Utilization Count</h4><blockquote>The Question: How often is the user living on the edge?</blockquote><p>High utilization (e.g., using 95% of a credit limit) isn’t always bad if it happens once. But repeated behavior suggests reliance on debt for liquidity.</p><ul><li><strong>The Logic:</strong> This counts the number of months where the user’s balance exceeded 90% of their credit limit.</li><li><strong>The Signal:</strong> This captures <strong>financial stress</strong>. Users who consistently max out their cards are statistically more sensitive to external economic shocks and less likely to recover from a missed payment.</li></ul><p>Here is a glimpse into the engine. Notice how we implement the logic in pure Python syntax, yet Numba compiles this down to optimized machine code. We handle stateful streaks and even manual linear regression slope calculations without the overhead of external libraries.</p><pre>import numpy as np<br>from numba import njit<br><br>@njit(cache=True)<br>def get_max_consecutive_late(pay_matrix):<br>    &quot;&quot;&quot;<br>    Calculates the longest streak of late payments per user.<br>    Logic: Resets counter to 0 immediately upon on-time payment.<br>    &quot;&quot;&quot;<br>    n_rows, n_cols = pay_matrix.shape<br>    result = np.zeros(n_rows, dtype=np.int32)<br><br>    for i in range(n_rows):<br>        max_streak = 0<br>        current_streak = 0<br>        for j in range(n_cols):<br>            if pay_matrix[i, j] &gt; 0: # 1 = Late<br>                current_streak += 1<br>            else:<br>                if current_streak &gt; max_streak:<br>                    max_streak = current_streak<br>                current_streak = 0 # Reset state<br>        <br>        # Final check for streak at the end of window<br>        if current_streak &gt; max_streak:<br>            max_streak = current_streak<br>        result[i] = max_streak<br>    return result<br><br>@njit(cache=True)<br>def get_velocity_slope(ratio_matrix):<br>    &quot;&quot;&quot;<br>    Computes linear trend (slope) of payment ratios manually.<br>    Hardcoded math for a fixed 3-month window avoids SciPy overhead.<br>    &quot;&quot;&quot;<br>    n_rows, n_cols = ratio_matrix.shape<br>    slopes = np.zeros(n_rows, dtype=np.float64)<br>    # Pre-calculated denominator for n=3 (x=[0,1,2])<br>    denom = 6.0 <br>    <br>    for i in range(n_rows):<br>        sum_y = 0.0<br>        sum_xy = 0.0<br>        for x in range(n_cols):<br>            y = ratio_matrix[i, x]<br>            if np.isnan(y) or np.isinf(y): y = 0.0 # Handle dirty data<br>            sum_y += y<br>            sum_xy += (x * y)<br><br>        # Manual slope formula: (n*sum(xy) - sum(x)*sum(y)) / denom<br>        num = (3.0 * sum_xy) - (3.0 * sum_y)<br>        slopes[i] = num / denom<br>    return slopes</pre><h4>Impact Study: Does it Actually Move the Needle?</h4><p>Engineering cool features is satisfying, but in credit risk, if it doesn’t improve the AUC (Area Under the Receiver Operating Characteristic Curve), it’s just technical debt. To measure the real-world value of these Numba-engineered signals, I conducted a controlled experiment using <strong>LightGBM</strong> (for non-linear interactions) and <strong>Logistic Regression</strong> (for linear interpretability).</p><p>I evaluated three distinct feature sets on the same hold-out test set:</p><ol><li><strong>Scenario A (Baseline):</strong> Standard magnitude aggregates only (e.g., <em>Mean Bill</em>, <em>Total Payment</em>).</li><li><strong>Scenario B (Behavioral Only):</strong> Only the Numba-engineered temporal features (e.g., <em>Streaks</em>, <em>Velocity</em>).</li><li><strong>Scenario C (Hybrid):</strong> The combination of both.</li></ol><p><strong>The Results</strong> The performance metrics painted a clear picture:</p><pre>========================================<br>FINAL LEADERBOARD<br>========================================<br>1. Hybrid AUC   : 0.6619 (+4.2%)<br>2. Baseline AUC : 0.6355<br>3. Behavior AUC : 0.6211 (-2.2%)<br>========================================</pre><p><strong>Analysis: The Power of Interaction</strong> At first glance, the Behavioral-only model (Scenario B) underperformed the Baseline. This is expected — knowing <em>how fast</em> someone is paying (velocity) isn’t useful if you don’t know <em>how much</em> they owe (magnitude).</p><p>However, the magic happens in <strong>Scenario C (Hybrid)</strong>. The combination achieved a <strong>+0.0265 AUC uplift (+4.2%)</strong>. This confirms that behavioral features act as powerful <strong>orthogonal signals</strong>. They provide context that static aggregates lack.</p><ul><li><em>High Debt</em> (Static) is bad.</li><li><em>High Debt</em> + <em>Negative Payment Velocity</em> (Behavioral) is <strong>catastrophic</strong>.</li></ul><p>The model learned these interactions. The SHAP analysis confirms our hypothesis. feat_pay_velocity and feat_max_late_streak didn’t just make the list—they ranked immediately after the primary financial aggregates. This proves the model isn&#39;t just &#39;using&#39; these features; it relies on them to capture the <strong>behavioral volatility</strong> that baseline averages completely miss.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bFuRqsDo-wvWTj8i6iXang.png" /><figcaption>Figure 3: Standard aggregates tell us <em>capacity</em>, but our engineered features tell us <em>character</em>. The high ranking of feat_max_late_streak proves that the model relies heavily on consistency patterns to detect default risk.</figcaption></figure><h4>Production Readiness: QA &amp; Stability</h4><p>In a regulated industry like Fintech, a broken pipeline is often worse than a non-existent one. High accuracy means nothing if the feature engine crashes on a generic edge case or if the model silently degrades due to data drift. To bridge the gap between “notebook prototype” and “industrial solution,” I implemented two layers of defense.</p><p><strong>1. Unit Testing the Numba Engine (</strong><strong>pytest)</strong> Since Numba JIT compiles Python code into machine code, debugging runtime errors can be opaque. To guarantee logical correctness, I built a rigorous test suite using pytest.</p><ul><li><strong>Edge Case Coverage:</strong> The tests specifically target data anomalies common in financial logs: users with <strong>zero bills</strong> (preventing division-by-zero errors in velocity calculations), <strong>NaN values</strong> (missing data), and <strong>infinite sequences</strong>.</li><li><strong>Logic Verification:</strong> I validated that the “Streak” logic correctly resets after a payment and handles rolling windows accurately across thousands of randomized synthetic test cases.</li></ul><p><strong>2. Drift Monitoring with PSI (Population Stability Index)</strong> A credit model is only as good as the data feeding it. If the distribution of “Late Streaks” changes drastically next month (e.g., due to a macroeconomic shift), the model needs to raise an alarm. I integrated a <strong>Population Stability Index (PSI)</strong> check to compare the distribution of engineered features between the training set and the test set.</p><ul><li><strong>The Threshold:</strong> Industry standard dictates that a PSI &lt; 0.1 is safe.</li><li><strong>Current Status:</strong> The pipeline is currently <strong>Green (Stable)</strong> with a PSI of <strong>0.0022</strong>.</li></ul><p>This monitoring layer ensures that the system is not just performant, but also robust enough to be deployed in a real-world, automated decision engine.</p><h4>Final Verdict</h4><p>There is often a misconception in data science that “complex engineering” is unnecessary overhead. This project proves the opposite: <strong>performance is a prerequisite for model intelligence.</strong></p><p>By optimizing the core feature engine with <strong>Numba</strong>, I didn’t just save time; I unlocked a new class of features. The 50,000x speedup meant I wasn’t forced to abandon stateful, temporal logic just because Pandas couldn’t handle it.</p><p>The impact is clear:</p><ol><li><strong>Speed:</strong> From ~58 seconds to ~1 millisecond per batch.</li><li><strong>Quality:</strong> From static aggregates to rich, behavioral signals.</li><li><strong>Value:</strong> A <strong>+4.2% AUC uplift</strong>, proving that the model was starving for this context.</li></ol><p><strong>My takeaway is simple:</strong> If your current tools (SQL or Pandas) are too slow to calculate the features you believe matter, do not “dumb down” the model. Optimize the engine. In the high-stakes world of credit risk, a 4.2% improvement isn’t just a metric — it’s a competitive advantage that directly impacts the bottom line.</p><p><strong>Explore the Code:</strong> The full source code, including the Numba optimization patterns and the validation pipeline, is available on GitHub. I invite you to fork the repo, break the benchmarks, and see if you can squeeze out even more performance.</p><p><a href="https://github.com/Muanai/credit-risk-feature-engine">GitHub - Muanai/credit-risk-feature-engine: Credit risk feature engine using Numba for stateful temporal logic, benchmarked for speed and validated by model impact.</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=eb9643908f9c" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[When Frogs Sing: Discovering Hidden Patterns with Unsupervised Learning]]></title>
            <link>https://medium.com/@muanaikhalifahr/when-frogs-sing-discovering-hidden-patterns-with-unsupervised-learning-cf79299c1ae4?source=rss-33bc6732a92e------2</link>
            <guid isPermaLink="false">https://medium.com/p/cf79299c1ae4</guid>
            <category><![CDATA[unsupervised-learning]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[bioacoustics]]></category>
            <category><![CDATA[k-means]]></category>
            <category><![CDATA[clustering]]></category>
            <dc:creator><![CDATA[Muanai Khalifah Revindo]]></dc:creator>
            <pubDate>Fri, 04 Jul 2025 13:06:58 GMT</pubDate>
            <atom:updated>2025-07-04T13:06:58.504Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How I used K-Means and t-SNE to uncover acoustic structure in frog species — without a single label.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*z_ROhUqnna7siv6A4c9zNA.jpeg" /></figure><h3>Ribbit Meets AI</h3><p>What do frogs and data science have in common? Turns out — quite a lot. In this project, I explored a real-world dataset of frog vocalizations, each broken down into 22 Mel-Frequency Cepstral Coefficients (MFCCs), and tried to uncover natural groupings of frog species using <strong>unsupervised clustering</strong>.</p><p>No labels. No supervision. Just raw frog croaks and some machine learning magic.</p><h3>The Dataset: Bioacoustics in the Wild</h3><p>Frog calls aren’t just cute background noise in the rainforest — they’re complex acoustic fingerprints. In this project, I explore how machine learning can uncover hidden clusters in their calls using only sound-based features.</p><p>This <a href="https://archive.ics.uci.edu/dataset/406/anuran+calls+mfccs">dataset</a> contains <strong>7,195 syllables</strong> extracted from <strong>10 frog species</strong> across <strong>4 families</strong>. Each instance is a frame of audio (or a “syllable”) with 22 MFCCs extracted from the waveform.</p><p>The original audio was recorded <strong>in situ</strong>, meaning the frogs weren’t in sterile labs — they were in the jungle, surrounded by real-world noise. This makes the problem more challenging (and exciting).</p><h4>Why MFCC?</h4><p>MFCCs are widely used in speech and audio processing as they capture the perceptual and structural aspects of sound. In the case of frogs, these coefficients encode crucial patterns in their calls — such as pitch, resonance, and rhythm — that are biologically and behaviorally distinctive.</p><h3>Goal: Can We Cluster These Croaks?</h3><p>Without using any label information, I wanted to:</p><ul><li>Cluster the 7,195 MFCC vectors</li><li>Evaluate how well those clusters align with the true species labels</li><li>Visualize the results with PCA and t-SNE</li></ul><h3>Preprocessing: A Little Help from PCA</h3><p>Before clustering, I used PCA to reduce dimensionality for visualization (and denoising):</p><pre>from sklearn.decomposition import PCA<br>pca = PCA(n_components=2)<br>X_pca = pca.fit_transform(X_scaled)</pre><h3>K-Means Clustering: Let the Grouping Begin</h3><p>I tried k from 2 to 10, and evaluated using <strong>Silhouette Score</strong>.</p><pre>from sklearn.cluster import KMeans<br>from sklearn.metrics import silhouette_score<br><br>silhouette_scores = []<br>for k in range(2, 11):<br>    km = KMeans(n_clusters=k, random_state=42)<br>    labels = km.fit_predict(X_scaled)<br>    silhouette_scores.append(silhouette_score(X_scaled, labels))</pre><p><strong>Best k: 5</strong>, with silhouette score = <strong>0.3543</strong></p><h3>Evaluation: Do Clusters Match Species?</h3><p>Using true species labels, I evaluated clustering quality with:</p><ul><li><strong>ARI (Adjusted Rand Index)</strong>: 0.7956</li><li><strong>NMI (Normalized Mutual Information)</strong>: 0.6660</li></ul><p>That’s surprisingly high, considering we used no label info!</p><h3>Visualizing with PCA</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/790/1*Zqk2hvMm7vBcQvL8DArrnQ.png" /></figure><p>Nice separation between clusters, but PCA struggles with non-linear structures. So…</p><h3>Enter t-SNE: Visualizing Frogs in 2D</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/790/1*M1yzmrgYraME8FMRtzkGUA.png" /></figure><p>Now that’s more like it. t-SNE revealed beautifully separated clusters, especially for species like:</p><ul><li><strong>AdenomeraHylaedactylus</strong> (dominated Cluster 0, ~97%)</li><li><strong>AdenomeraAndre</strong> (88% of Cluster 2)</li><li><strong>HypsiboasCordobae</strong> (52% of Cluster 4)</li></ul><p>But what does this mean?</p><h3>Interpreting the Frogs’ Hidden Dialects</h3><p>Looking deeper into the cluster composition:</p><ul><li><strong>Cluster 0</strong> is practically a perfect island for <em>AdenomeraHylaedactylus</em>. Its acoustic profile must be extremely unique — potentially due to distinct syllable length or harmonic patterns.</li><li><strong>Cluster 2</strong> catches <em>AdenomeraAndre</em> almost exclusively. It seems this genus has a separate acoustic signature, distinguishable even from its sibling genus.</li><li><strong>Cluster 4</strong> is more mixed, but still reflects strong presence of <em>HypsiboasCordobae</em>. That genus might share features with others but still forms a meaningful core.</li></ul><p>The <strong>lower-purity clusters</strong> (like Cluster 3) suggest:</p><ul><li>Acoustic overlap between different genera (possibly due to shared environments or mimicry)</li><li>Limitations of MFCCs in distinguishing species with similar call structures</li></ul><p>This result supports the <strong>bioacoustic hypothesis</strong>: that frog calls are evolutionarily shaped to stand out within ecosystems, but may still overlap where habitats or behaviors converge.</p><h3>Technical Takeaways</h3><ul><li><strong>High ARI (0.7956)</strong> means K-Means managed to reconstruct true species labels quite accurately.</li><li><strong>t-SNE</strong> captured the local structure of MFCC space, making acoustic boundaries visible.</li><li><strong>K-Means + PCA + MFCCs</strong> forms a surprisingly strong baseline pipeline for acoustic clustering.</li></ul><h3>Final Thoughts</h3><p>Nature sings, and machine learning listens.</p><p>This project showed how unsupervised learning can uncover natural structure in animal behavior — even without labels. Whether you’re into frogs, sound, or unsupervised learning, there’s a lot more to explore.</p><p>Curious about how frogs form clusters?</p><p>👉 <a href="https://colab.research.google.com/drive/1kB7cBDdErl0GTZmIU3IxFa7FH9RY5Wqp?usp=sharing">Run the interactive notebook on Google Colab</a></p><p>Thanks for reading!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cf79299c1ae4" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>