<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thisal Dilmith</title>
    <description>The latest articles on DEV Community by Thisal Dilmith (@thisald).</description>
    <link>https://dev.to/thisald</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1303377%2F26dc9a05-2faf-434d-9be9-928a3b35c74f.jpg</url>
      <title>DEV Community: Thisal Dilmith</title>
      <link>https://dev.to/thisald</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thisald"/>
    <language>en</language>
    <item>
      <title>How I Built a Live-Updating Line Chart Widget for Tkinter Without Any External Dependencies</title>
      <dc:creator>Thisal Dilmith</dc:creator>
      <pubDate>Fri, 26 Jun 2026 18:11:30 +0000</pubDate>
      <link>https://dev.to/thisald/how-i-built-a-live-updating-line-chart-widget-for-tkinter-without-any-external-dependencies-1pj6</link>
      <guid>https://dev.to/thisald/how-i-built-a-live-updating-line-chart-widget-for-tkinter-without-any-external-dependencies-1pj6</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Tkinter ships with enough widgets to build a functional desktop GUI in an afternoon. &lt;br&gt;
What it doesn't ship with is any built-in way to display data that changes over time. &lt;br&gt;
If you're building a CPU monitor, a sensor dashboard, or any tool that needs to &lt;br&gt;
visualize a live stream of values, you have two realistic options: embed matplotlib &lt;br&gt;
in a &lt;code&gt;FigureCanvasTkAgg&lt;/code&gt;, or roll your own canvas drawing logic. The first option &lt;br&gt;
works but pulls in a dependency that's larger than most projects need. The second &lt;br&gt;
option means rebuilding the same axis, scaling, and rendering logic every time.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Existing Solutions Didn't Cut It
&lt;/h2&gt;

&lt;p&gt;The matplotlib-in-Tkinter approach is the most commonly recommended solution, and &lt;br&gt;
it's fine for static charts. For live data though, it has friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You manage figure/canvas lifecycle manually.&lt;/li&gt;
&lt;li&gt;Animation via &lt;code&gt;FuncAnimation&lt;/code&gt; fights with Tkinter's event loop unless you're 
careful with &lt;code&gt;blit=True&lt;/code&gt; and backend selection.&lt;/li&gt;
&lt;li&gt;The import footprint (&lt;code&gt;numpy&lt;/code&gt;, &lt;code&gt;matplotlib&lt;/code&gt;) is heavy for an app whose chart is 
a minor feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other option — drawing on a &lt;code&gt;tk.Canvas&lt;/code&gt; directly — is fine at small scale but &lt;br&gt;
requires you to reimplement axis labels, scaling, grid lines, and multi-line &lt;br&gt;
coordination every single time.&lt;/p&gt;

&lt;p&gt;What I wanted: a &lt;code&gt;LineChart&lt;/code&gt; class I could drop into any Tkinter app the same way &lt;br&gt;
I'd drop in a &lt;code&gt;ttk.Treeview&lt;/code&gt;. Create it, pack it, feed it data. Done.&lt;/p&gt;


&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

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

&lt;p&gt;tkchart exposes two classes: &lt;code&gt;LineChart&lt;/code&gt; (the widget) and &lt;code&gt;Line&lt;/code&gt; (a data series &lt;br&gt;
attached to a chart).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tkchart&lt;/span&gt;

&lt;span class="n"&gt;chart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tkchart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LineChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;x_axis_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;y_axis_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;y_axis_section_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;x_axis_section_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pady&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tkchart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#5dffb6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dashed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;style_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;LineChart&lt;/code&gt; owns the canvas, axes, labels, and grid. &lt;code&gt;Line&lt;/code&gt; is a lightweight &lt;br&gt;
descriptor — it holds style properties and a data buffer, but the chart controls &lt;br&gt;
all rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feeding data&lt;/strong&gt; happens via &lt;code&gt;show_data()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;This is designed to be called from a background thread. Internally, canvas &lt;br&gt;
operations are dispatched to the main thread via Tkinter's &lt;code&gt;after()&lt;/code&gt; mechanism — &lt;br&gt;
the caller doesn't have to think about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key architectural decisions:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decoupled &lt;code&gt;Line&lt;/code&gt; from &lt;code&gt;LineChart&lt;/code&gt;&lt;/strong&gt;: Each &lt;code&gt;Line&lt;/code&gt; maintains its own data &lt;br&gt;
buffer independently. &lt;code&gt;get_line_data()&lt;/code&gt;, &lt;code&gt;get_current_visible_data()&lt;/code&gt;, and &lt;br&gt;
related methods let you query what's on screen at any point — useful for &lt;br&gt;
triggering alerts or logging snapshots.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scrolling X-axis&lt;/strong&gt;: As data arrives, the X-axis label set scrolls. The &lt;br&gt;
&lt;code&gt;x_axis_values&lt;/code&gt; tuple defines the visible label template, not a fixed dataset. &lt;br&gt;
This means the chart is conceptually infinite on the time axis.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Runtime reconfiguration&lt;/strong&gt;: v2.2.0 added &lt;code&gt;configure_*()&lt;/code&gt; methods for almost &lt;br&gt;
every visual property. You can change axis colors, pointer behavior, or &lt;br&gt;
line fill at runtime without destroying and recreating the widget.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;   &lt;span class="n"&gt;chart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure_bg_color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#1a1a2e&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure_color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#ff6b9d&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure_fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enabled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pointer with callback&lt;/strong&gt;: An optional hover pointer shows interpolated values 
at cursor position and fires a user-supplied callback function — so you can 
wire it to a label or trigger an action based on which data point is hovered.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  One Thing That Surprised Me
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;show_data()&lt;/code&gt; call accepts a list, not a single value. I intended this to &lt;br&gt;
support batch inserts — you can push multiple data points in one call, and the &lt;br&gt;
chart will render them all in sequence.&lt;/p&gt;

&lt;p&gt;The tricky part: when multiple &lt;code&gt;Line&lt;/code&gt; objects share the same &lt;code&gt;LineChart&lt;/code&gt;, their &lt;br&gt;
data lengths need to stay synchronized for the X-axis to remain coherent. The &lt;br&gt;
chart uses the maximum data length across all lines as its internal clock. If one &lt;br&gt;
line accumulates data faster than another, the slower line's visible portion gets &lt;br&gt;
padded implicitly.&lt;/p&gt;

&lt;p&gt;This means callers have to be deliberate about calling &lt;code&gt;show_data()&lt;/code&gt; at consistent &lt;br&gt;
rates across all lines if they want correct synchronization. It works well when all &lt;br&gt;
lines are driven from the same loop (the common case), but it's a real footgun if &lt;br&gt;
you have two independent threads pushing to two separate lines at different intervals.&lt;/p&gt;

&lt;p&gt;I haven't found a clean solution that doesn't add per-line timestamps and complicate &lt;br&gt;
the rendering model significantly. For now, the docs recommend keeping all &lt;br&gt;
&lt;code&gt;show_data()&lt;/code&gt; calls inside a single loop.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bar chart support&lt;/strong&gt;: The &lt;code&gt;LineChart&lt;/code&gt; architecture is canvas-based enough that 
adding a &lt;code&gt;BarChart&lt;/code&gt; class is feasible. The axis and label system could be shared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export&lt;/strong&gt;: A method to snapshot the current canvas state to a PNG. The 
&lt;code&gt;tk.Canvas.postscript()&lt;/code&gt; method gets close but requires an extra conversion step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed stubs&lt;/strong&gt;: The codebase predates type hints. Adding &lt;code&gt;.pyi&lt;/code&gt; stub files would 
make autocomplete and mypy integration much better.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Call to Action
&lt;/h2&gt;

&lt;p&gt;The design decision I'm least certain about: &lt;strong&gt;the &lt;code&gt;Line&lt;/code&gt;-as-descriptor pattern&lt;/strong&gt; &lt;br&gt;
where &lt;code&gt;Line&lt;/code&gt; holds style but &lt;code&gt;LineChart&lt;/code&gt; owns all rendering. It keeps the rendering &lt;br&gt;
logic centralized, but it means &lt;code&gt;Line&lt;/code&gt; objects are inert outside the context of their &lt;br&gt;
parent chart.&lt;/p&gt;

&lt;p&gt;An alternative would be to make &lt;code&gt;Line&lt;/code&gt; a proper canvas actor that draws itself — &lt;br&gt;
closer to how matplotlib's &lt;code&gt;Artist&lt;/code&gt; hierarchy works. That would allow lines to be &lt;br&gt;
moved between charts, but it would also scatter the rendering logic.&lt;/p&gt;

&lt;p&gt;If you've designed a similar multi-series chart component — in any language or &lt;br&gt;
framework — I'd genuinely like to hear which pattern held up better over time: &lt;br&gt;
centralized renderer or autonomous actors.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/thisal-d/tkchart" rel="noopener noreferrer"&gt;https://github.com/thisal-d/tkchart&lt;/a&gt;&lt;br&gt;&lt;br&gt;
PyPI: &lt;a href="https://pypi.org/project/tkchart/" rel="noopener noreferrer"&gt;https://pypi.org/project/tkchart/&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;PyPI: &lt;code&gt;pip install tkchart&lt;/code&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>tkinter</category>
      <category>customtkinter</category>
      <category>opensource</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Thisal Dilmith</dc:creator>
      <pubDate>Fri, 26 Jun 2026 14:58:40 +0000</pubDate>
      <link>https://dev.to/thisald/-m11</link>
      <guid>https://dev.to/thisald/-m11</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/thisald/why-gridsearchcv-wastes-most-of-its-time-and-what-i-did-about-it-2cak" class="crayons-story__hidden-navigation-link"&gt;Why GridSearchCV Wastes Most of Its Time — And What I Did About It&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/thisald" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1303377%2F26dc9a05-2faf-434d-9be9-928a3b35c74f.jpg" alt="thisald profile" class="crayons-avatar__image" width="800" height="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/thisald" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Thisal Dilmith
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Thisal Dilmith
                
              
              &lt;div id="story-author-preview-content-3991994" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/thisald" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1303377%2F26dc9a05-2faf-434d-9be9-928a3b35c74f.jpg" class="crayons-avatar__image" alt="" width="800" height="800"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Thisal Dilmith&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/thisald/why-gridsearchcv-wastes-most-of-its-time-and-what-i-did-about-it-2cak" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jun 25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/thisald/why-gridsearchcv-wastes-most-of-its-time-and-what-i-did-about-it-2cak" id="article-link-3991994"&gt;
          Why GridSearchCV Wastes Most of Its Time — And What I Did About It
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/machinelearning"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;machinelearning&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/datascience"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;datascience&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/thisald/why-gridsearchcv-wastes-most-of-its-time-and-what-i-did-about-it-2cak" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;2&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/thisald/why-gridsearchcv-wastes-most-of-its-time-and-what-i-did-about-it-2cak#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Why GridSearchCV Wastes Most of Its Time — And What I Did About It</title>
      <dc:creator>Thisal Dilmith</dc:creator>
      <pubDate>Thu, 25 Jun 2026 20:16:05 +0000</pubDate>
      <link>https://dev.to/thisald/why-gridsearchcv-wastes-most-of-its-time-and-what-i-did-about-it-2cak</link>
      <guid>https://dev.to/thisald/why-gridsearchcv-wastes-most-of-its-time-and-what-i-did-about-it-2cak</guid>
      <description>&lt;p&gt;If you've ever tuned hyperparameters on a large grid, you know the pain. You kick off a &lt;code&gt;GridSearchCV&lt;/code&gt;, go make coffee, come back, and it's still running. Maybe you go to lunch. Maybe it's still running.&lt;/p&gt;

&lt;p&gt;I got frustrated enough to build something different. It's called &lt;a href="https://github.com/thisal-d/elimination-search-cv" rel="noopener noreferrer"&gt;EliminationSearchCV&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with GridSearchCV
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;GridSearchCV&lt;/code&gt; is brute force by design. For a grid with &lt;code&gt;k&lt;/code&gt; parameters and &lt;code&gt;n&lt;/code&gt; values each, it evaluates &lt;strong&gt;nᵏ × cv_folds&lt;/strong&gt; configurations — every single one, regardless of how poorly a value performs early on.&lt;/p&gt;

&lt;p&gt;Here's what that actually means in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;Impact&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dead-end values are never discarded&lt;/td&gt;
&lt;td&gt;A bad &lt;code&gt;learning_rate=0.5&lt;/code&gt; is re-evaluated in every downstream combination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No learning from early results&lt;/td&gt;
&lt;td&gt;The search treats round 1 and round 1000 as equally uninformed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exponential cost scaling&lt;/td&gt;
&lt;td&gt;Adding one new 4-value parameter can &lt;strong&gt;quadruple&lt;/strong&gt; total training time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last point is the killer. Your grid doesn't have to be huge for this to hurt — it just has to grow.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea: Eliminate Instead of Enumerate
&lt;/h2&gt;

&lt;p&gt;What if instead of evaluating everything upfront, we tested parameter values in rounds — and dropped the bad ones before they compound?&lt;/p&gt;

&lt;p&gt;That's &lt;code&gt;EliminationSearchCV&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It works like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Round 1:&lt;/strong&gt; Test each parameter value &lt;em&gt;in isolation&lt;/em&gt;. Score them per-parameter and eliminate the worst performers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round 2:&lt;/strong&gt; Test surviving pairs. Rank all combinations globally, keep the top fraction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Round 3+:&lt;/strong&gt; Repeat with triples, then full combinations, until one winner remains.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bad values get cut early. They never get the chance to multiply into thousands of useless combinations.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Concrete Example
&lt;/h2&gt;

&lt;p&gt;Let's tune a &lt;code&gt;LogisticRegression&lt;/code&gt; with 4 parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;param_grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;C&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;   &lt;span class="c1"&gt;# 6 values
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;penalty&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;l1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;l2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                       &lt;span class="c1"&gt;# 2 values
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;solver&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;liblinear&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;saga&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;              &lt;span class="c1"&gt;# 2 values
&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;max_iter&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;                       &lt;span class="c1"&gt;# 2 values
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# GridSearchCV: 6 × 2 × 2 × 2 = 48 combos × 5 folds = 240 fits
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;EliminationSearchCV&lt;/code&gt; and &lt;code&gt;elimination_rate=0.8&lt;/code&gt; (keep best 20%):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Round&lt;/th&gt;
&lt;th&gt;Combos tested&lt;/th&gt;
&lt;th&gt;Grid after elimination&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 — single params&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;&lt;code&gt;C:[1], penalty:['l1'], solver:['liblinear'], max_iter:[1000]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 — pairs&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;unchanged (already 1 value each)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 — triples&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 — full&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;final result&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23 fits&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;vs 240 for GridSearchCV&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same best params. A fraction of the work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Drop-in Replacement
&lt;/h2&gt;

&lt;p&gt;The API is intentionally identical to &lt;code&gt;GridSearchCV&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;EliminationSearchCV&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;EliminationSearchCV&lt;/span&gt;

&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GridSearchCV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;param_grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After — just swap the class name
&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EliminationSearchCV&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;estimator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;param_grid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;param_grid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scoring&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;accuracy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cv&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;elimination_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# eliminate worst 80% each round
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Same interface as GridSearchCV
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_params_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → {'C': 1, 'penalty': 'l1', 'solver': 'liblinear', 'max_iter': 1000}
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_score_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# → 0.9248
&lt;/span&gt;
&lt;span class="c1"&gt;# Already refitted on full training set — ready to predict
&lt;/span&gt;&lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;best_estimator_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  One Thing I'm Proud Of: Invalid Combo Handling
&lt;/h2&gt;

&lt;p&gt;Sklearn can raise errors for incompatible combinations — like &lt;code&gt;penalty='l1'&lt;/code&gt; with &lt;code&gt;solver='lbfgs'&lt;/code&gt;. GridSearchCV crashes on these. You have to manually filter them out.&lt;/p&gt;

&lt;p&gt;EliminationSearchCV catches any exception during &lt;code&gt;fit()&lt;/code&gt;, scores that combination &lt;code&gt;0.0&lt;/code&gt;, and lets the elimination logic handle it naturally. Invalid combos just die in Round 1. No special handling needed from you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark Results
&lt;/h2&gt;

&lt;p&gt;Tested across 5 models and 3 datasets (cv=2, elimination_rate=0.8, 10,000 samples):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Grid&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;th&gt;Accuracy diff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DecisionTree&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;152x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.0008&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RandomForest&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.0002&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GradientBoosting&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;35x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.0194&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;KNeighbors&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;11x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.0004&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LogisticRegression&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4x&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-0.0004&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Full grids are where this shines. The accuracy trade-off is minimal — under 0.02 across all models, often zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honest caveat:&lt;/strong&gt; Light grids (small search spaces) are actually &lt;em&gt;slower&lt;/em&gt; with this approach. The elimination overhead doesn't pay off when there are only a few combinations to begin with. If your grid is small, stick with GridSearchCV.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: How It's Built
&lt;/h2&gt;

&lt;p&gt;The library is two files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/EliminationSearchCV/
├── EliminationSearchCV.py   ← Core class: fit(), elimination logic, scoring
└── Utils.py                 ← Stateless utilities: fold creation, combination generation, metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flow inside &lt;code&gt;fit()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EliminationSearchCV.fit(X, y)
    │
    ├─▶ Utils.create_cv_data_sets()         — StratifiedKFold/KFold splits
    │
    └─▶ [For each round i = 1 … n_params]
             │
             ├─▶ generate_param_combinations_with_limit(grid, limit=i)
             │
             ├─▶ _score_candidates(candidates)
             │         — per-fold metric evaluation
             │
             └─▶ _eliminate_low_scoring_values(candidates, scores)
                       ├─▶ _eliminate_single_param_values()   — Round 1
                       └─▶ _eliminate_multi_param_values()    — Rounds 2+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A key design decision: in Round 1, each parameter's values are scored and compared &lt;em&gt;in isolation&lt;/em&gt; — so &lt;code&gt;C&lt;/code&gt; values compete only against other &lt;code&gt;C&lt;/code&gt; values, not against &lt;code&gt;penalty&lt;/code&gt; values. This prevents interference between parameters that are on completely different scales.&lt;/p&gt;

&lt;p&gt;In later rounds, all combinations are ranked globally and the top &lt;code&gt;(1 - elimination_rate)&lt;/code&gt; fraction survives.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Working and What's Not Yet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Currently supported:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fit()&lt;/code&gt;, &lt;code&gt;best_params_&lt;/code&gt;, &lt;code&gt;best_score_&lt;/code&gt;, &lt;code&gt;best_estimator_&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Round 1: per-parameter isolation and elimination&lt;/li&gt;
&lt;li&gt;Rounds 2+: global combination ranking&lt;/li&gt;
&lt;li&gt;StratifiedKFold / KFold cross-validation&lt;/li&gt;
&lt;li&gt;Invalid combination handling&lt;/li&gt;
&lt;li&gt;Scoring: &lt;code&gt;accuracy&lt;/code&gt;, &lt;code&gt;precision&lt;/code&gt;, &lt;code&gt;recall&lt;/code&gt;, &lt;code&gt;f1&lt;/code&gt;, &lt;code&gt;roc_auc&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;On the roadmap:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cv_results_&lt;/code&gt; (per-fold score breakdown)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n_jobs&lt;/code&gt; parallel evaluation via joblib&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;verbose&lt;/code&gt; logging&lt;/li&gt;
&lt;li&gt;Full pytest test suite&lt;/li&gt;
&lt;li&gt;Scikit-learn &lt;code&gt;BaseEstimator&lt;/code&gt; compatibility&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;elimination-search-cv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Requirements: Python ≥ 3.8. scikit-learn and numpy install automatically.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/thisal-d/elimination-search-cv" rel="noopener noreferrer"&gt;https://github.com/thisal-d/elimination-search-cv&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Honest Disclaimer
&lt;/h2&gt;

&lt;p&gt;This is an experimental approach. The quality of results depends heavily on the dataset and model. I'm actively benchmarking it and the results so far are promising — but I wouldn't call it production-ready yet.&lt;/p&gt;

&lt;p&gt;What I'd genuinely love is feedback on edge cases where it fails. If you try it on a grid where it gives clearly wrong results or behaves unexpectedly, please open an issue. That's more useful to me right now than praise.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this interesting, a ⭐ on the repo helps a lot — it keeps the motivation alive to keep building.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;python&lt;/code&gt; &lt;code&gt;machinelearning&lt;/code&gt; &lt;code&gt;datascience&lt;/code&gt; &lt;code&gt;opensource&lt;/code&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
