<?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>Forem: Iman</title>
    <description>The latest articles on Forem by Iman (@imann_12).</description>
    <link>https://forem.com/imann_12</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3932861%2F50daf84d-73cc-4c5a-86ae-07c626c3a979.png</url>
      <title>Forem: Iman</title>
      <link>https://forem.com/imann_12</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/imann_12"/>
    <language>en</language>
    <item>
      <title>Stop Using .iterrows(). Here's What Actually Fast Looks Like</title>
      <dc:creator>Iman</dc:creator>
      <pubDate>Thu, 21 May 2026 15:24:09 +0000</pubDate>
      <link>https://forem.com/imann_12/stop-using-iterrows-heres-what-actually-fast-looks-like-3j17</link>
      <guid>https://forem.com/imann_12/stop-using-iterrows-heres-what-actually-fast-looks-like-3j17</guid>
      <description>&lt;p&gt;You're looping over a DataFrame. It feels natural. It's killing your performance.&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="c1"&gt;# What most tutorials say
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iterrows&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.17&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the progression you should actually know:&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="c1"&gt;# Level 1: Vectorization — 10-100x faster 
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.17&lt;/span&gt;

&lt;span class="c1"&gt;# Level 2: .apply() when logic is conditional 
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.17&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Level 3: np.where — the fastest option 
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.17&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;1M rows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.iterrows()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~480s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.apply()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~3s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vectorized / &lt;code&gt;np.where&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;~0.04s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pandas wraps NumPy. NumPy operates on entire arrays at the C level. The moment you loop row by row, you throw that away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shift:&lt;/strong&gt; don't think &lt;em&gt;"what do I do to each row?"&lt;/em&gt; rather you should ask &lt;em&gt;"what transformation applies to this column?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's it. Notebooks that took minutes will now run in seconds.&lt;/p&gt;




</description>
      <category>datascience</category>
      <category>python</category>
      <category>pandas</category>
    </item>
    <item>
      <title>Why Your Windows Paths Break Inside a Docker Container (and How to Fix It in .NET)</title>
      <dc:creator>Iman</dc:creator>
      <pubDate>Sun, 17 May 2026 15:59:22 +0000</pubDate>
      <link>https://forem.com/imann_12/why-your-windows-paths-break-inside-a-docker-container-and-how-to-fix-it-in-net-115p</link>
      <guid>https://forem.com/imann_12/why-your-windows-paths-break-inside-a-docker-container-and-how-to-fix-it-in-net-115p</guid>
      <description>&lt;p&gt;If you have ever deployed a .NET app inside a Docker container on a Windows host, you have probably run into a situation where a path that looks perfectly valid on the host machine causes subtle, hard-to-debug failures inside the container. This post walks through the exact problem, the runtime behaviour that causes it, and a one-line fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I was building &lt;a href="https://github.com/imann128/DevMetrics" rel="noopener noreferrer"&gt;DevMetrics&lt;/a&gt;, a self-hosted developer productivity dashboard. Users add local Git repositories through a web form by pasting in the path. The app then calls LibGit2Sharp to scan commits from that path.&lt;/p&gt;

&lt;p&gt;On Windows, running via &lt;code&gt;dotnet run&lt;/code&gt;, everything worked fine. After dockerizing the app and running it as a Linux container, paths started silently mangling themselves.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Symptom
&lt;/h2&gt;

&lt;p&gt;A user pastes this into the form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;D:\Users\Downloads\my-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app logs show the path it actually tried to open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/app/D:\Users\Downloads\my-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The working directory &lt;code&gt;/app&lt;/code&gt; had been prepended to the Windows path. LibGit2Sharp then throws because that path obviously does not exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why It Happens
&lt;/h2&gt;

&lt;p&gt;The culprit is &lt;code&gt;Path.GetFullPath&lt;/code&gt;. In .NET, calling &lt;code&gt;GetFullPath&lt;/code&gt; on a relative path resolves it against the current working directory. The relevant question is: what counts as "rooted" on Linux?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On Windows: returns true&lt;/span&gt;
&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsPathRooted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"D:\\Users\\Downloads\\my-project"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// On Linux: returns FALSE&lt;/span&gt;
&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsPathRooted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"D:\\Users\\Downloads\\my-project"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Linux has no concept of Windows drive letters. To the Linux runtime, &lt;code&gt;D:\Users\Downloads\my-project&lt;/code&gt; is not an absolute path starting with a drive letter. It is a relative path that happens to start with the character &lt;code&gt;D&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So when you call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFullPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"D:\\Users\\Downloads\\my-project"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;on Linux, the runtime treats it as relative and prepends the process working directory, giving you &lt;code&gt;/app/D:\Users\Downloads\my-project&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No exception is thrown. No warning is logged. The path just silently becomes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Guard with &lt;code&gt;IsPathRooted&lt;/code&gt; before calling &lt;code&gt;GetFullPath&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;NormalisePath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;absolute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsPathRooted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetFullPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;absolute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TrimEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DirectorySeparatorChar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AltDirectorySeparatorChar&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;If &lt;code&gt;IsPathRooted&lt;/code&gt; returns false (which it will for any Windows-style path on Linux), skip &lt;code&gt;GetFullPath&lt;/code&gt; entirely and use the trimmed value as-is. The path will still be wrong in the sense that &lt;code&gt;D:\...&lt;/code&gt; is not a valid Linux path, but at least you have not silently corrupted it further. You can then validate it properly and return a clear error to the user.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Deeper Problem: Host Paths vs Container Paths
&lt;/h2&gt;

&lt;p&gt;Even with the fix above, there is a second issue worth understanding. When you run Docker on Windows, the paths inside the container are Linux paths, not Windows paths.&lt;/p&gt;

&lt;p&gt;If your &lt;code&gt;docker-compose.yml&lt;/code&gt; mounts a host directory like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;D:\Users\Downloads\my-project:/repos/my-project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the container, that directory is available at &lt;code&gt;/repos/my-project&lt;/code&gt;. The Windows path &lt;code&gt;D:\Users\Downloads\my-project&lt;/code&gt; does not exist from the container's perspective at all.&lt;/p&gt;

&lt;p&gt;So the correct path for a user to enter in your app's form is &lt;code&gt;/repos/my-project&lt;/code&gt;, not the Windows path they see in File Explorer.&lt;/p&gt;

&lt;p&gt;This is worth making explicit in your UI. In DevMetrics I added a hint directly on the Add Repository form:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In Docker, use the container path (e.g. &lt;code&gt;/repos/my-project&lt;/code&gt;).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A one-line hint that prevents a lot of confusion.&lt;/p&gt;




&lt;p&gt;The rule is simple: &lt;code&gt;IsPathRooted&lt;/code&gt; is OS-aware. A Windows drive-letter path is not considered rooted on Linux, and &lt;code&gt;GetFullPath&lt;/code&gt; will silently corrupt it as a result.&lt;/p&gt;




</description>
      <category>docker</category>
      <category>csharp</category>
      <category>linux</category>
      <category>learning</category>
    </item>
    <item>
      <title>How Analyzing Stock Market Data Taught Me What Time Series Textbooks Couldn't</title>
      <dc:creator>Iman</dc:creator>
      <pubDate>Sat, 16 May 2026 10:57:05 +0000</pubDate>
      <link>https://forem.com/imann_12/how-analyzing-stock-market-data-taught-me-what-time-series-textbooks-couldnt-1kd9</link>
      <guid>https://forem.com/imann_12/how-analyzing-stock-market-data-taught-me-what-time-series-textbooks-couldnt-1kd9</guid>
      <description>&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.amazonaws.com%2Fuploads%2Farticles%2Fkh1kfdapco241q6skzr8.png" 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.amazonaws.com%2Fuploads%2Farticles%2Fkh1kfdapco241q6skzr8.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I spent a semester analyzing OGDC — Pakistan's largest oil and gas company — on the Pakistan Stock Exchange. Financial data breaks every assumption you were taught to rely on. This is what that actually looks like in practice.&lt;/p&gt;

&lt;p&gt;Classroom Time Series: Deceptively Plain &lt;br&gt;
When you first learn time series analysis, you get clean examples. A seasonal temperature dataset. A sales series that trends upward. The examples are chosen because the methods work on them.&lt;br&gt;
Real financial data doesn't do that.&lt;br&gt;
The first thing I did was plot OGDC's daily returns and run a normality test. The Jarque-Bera statistic came back at 172,348. The p-value was effectively zero. Excess kurtosis was 41.7.&lt;br&gt;
A normally distributed series has kurtosis of 0. A kurtosis of 41.7 means the tails are so fat that standard deviation becomes a nearly meaningless risk metric. Extreme daily moves — the kind that would be essentially impossible under a Gaussian model — were happening regularly.&lt;br&gt;
That number forced me to actually understand what kurtosis means instead of just knowing it's the fourth moment of a distribution. There's a difference.&lt;/p&gt;

&lt;p&gt;Stationarity Is Not Academic&lt;br&gt;
Every time series textbook starts with stationarity. Run the ADF test, check the p-value, and proceed. I did this robotically for two years before working with financial data made it concrete.&lt;br&gt;
OGDC's price series has a unit root — it's non-stationary. The returns series is stationary. This distinction matters enormously because:&lt;/p&gt;

&lt;p&gt;You cannot apply ARIMA to a non-stationary series without differencing it&lt;br&gt;
If you use price levels as your ML target, you're teaching your model to predict a random walk with drift — it will learn "tomorrow's price ≈ today's price," score well on R², and be completely useless&lt;br&gt;
Feature engineering on price levels creates look-ahead bias in ways that are subtle and easy to miss&lt;/p&gt;

&lt;p&gt;Once I understood why we model returns instead of prices — not because a textbook said so, but because I watched what happened when I tried to model prices — stationarity became a tool rather than a checkbox.&lt;/p&gt;

&lt;p&gt;Autocorrelation Actually Tells You Something&lt;br&gt;
I ran a Ljung-Box test on the return series and found significant autocorrelation at lags 1, 3, 12, 22, and 29. Then I ran a Runs Test and found that the signs of returns — whether each day is up or down — are completely random (Z = 0.043, p = 0.97).&lt;br&gt;
These two results together are more interesting than either one alone.&lt;br&gt;
There is short-horizon autocorrelation in the magnitude and sequence of returns, but no predictability in direction. The Runs Test is essentially a direct test of the Efficient Market Hypothesis at the binary level. OGDC passes it — you cannot predict whether tomorrow is an up day from knowing today was an up day.&lt;br&gt;
The ARMA(0,3) model I fit finds genuine structure — three moving-average terms capturing autocorrelation at those specific lags, with residuals that pass all diagnostic tests. The model is adequate. It just can't forecast direction, only structure.&lt;br&gt;
That distinction — structure vs predictability — is something I didn't appreciate until I saw it in data where it actually mattered.&lt;/p&gt;

&lt;p&gt;Volatility Is A Time Series Too&lt;br&gt;
The most important thing I built in this project was not a price prediction model. It was a volatility model.&lt;br&gt;
The Ljung-Box test on squared returns came back with Q(10) = 226.96 (p ≈ 0). Volatility was clustering — large moves followed by large moves, regardless of direction. This is the ARCH effect. It means a constant-variance assumption in any model you build on this data is simply wrong.&lt;br&gt;
I implemented four GARCH family models from scratch using scipy.optimize for maximum likelihood estimation — ARCH(1), GARCH(1,1), EGARCH(1,1), and GJR-GARCH(1,1). No external library.&lt;br&gt;
GJR-GARCH won by AIC. The leverage parameter γ = 0.33 means negative shocks amplify future volatility 33% more than positive shocks of equal magnitude. Seeing that emerge from your own MLE implementation — watching the optimizer converge to a parameter that confirms something the financial econometrics literature established decades ago — is a different kind of understanding than reading about it.&lt;br&gt;
The persistence parameter came out at α+β = 0.78, implying a shock half-life of 2.8 trading days. Developed market equities typically show half-lives of 20–30 days. PSX stocks mean-revert faster, which is consistent with thinner liquidity and more retail-driven price discovery.&lt;/p&gt;

&lt;p&gt;The Machine Learning Part Humbled Me&lt;br&gt;
I built a Random Forest classifier to predict daily price direction. It achieved 99.7% accuracy and ROC-AUC above 0.999.&lt;br&gt;
I spent about an hour thinking I had done something impressive before I figured out what had actually happened.&lt;br&gt;
The raw data from Investing.com includes a Change % column — the same-day percentage change. I had included it as a feature. It is numerically equivalent to the classification target I was trying to predict. The model had learned to read the answer from the question paper.&lt;br&gt;
When I removed it and all other same-day OHLC data, accuracy dropped to 53–54%. That's the honest number. Still above the 51.3% majority-class baseline — a real but modest edge consistent with the short-horizon autocorrelation the ARMA model detected. But nowhere near 99.7%.&lt;br&gt;
Data leakage is easy to understand abstractly and much harder to catch in practice, especially when it comes from a column that looks obviously useful and has a name that doesn't immediately suggest it contains the target variable. I caught it because the accuracy was too high. If it had been 72%, I might never have looked.&lt;/p&gt;

&lt;p&gt;What Financial Data Forces You To Learn&lt;br&gt;
Working with market data has specific advantages over other domains as a learning environment.&lt;br&gt;
Everything is measurable. You can immediately test whether a finding is statistically meaningful — run a Diebold-Mariano test, get a direct answer. The ground truth is public and continuous, producing a new observation every trading day. The feedback loop is tight in a way most applied ML projects aren't.&lt;br&gt;
More importantly, the assumptions of standard methods are genuinely violated. Non-normality, heteroscedasticity, autocorrelation, structural breaks — financial data has all of them simultaneously. You cannot take shortcuts and get away with it the way you can with cleaner data. And the cost of ignoring violations is concrete: a risk model assuming Normal returns will underestimate the probability of a -10% day by an order of magnitude. That's not theoretical. It's the kind of mistake that has real consequences.&lt;/p&gt;

&lt;p&gt;What I Would Tell Myself Before Starting&lt;br&gt;
The statistical tests are not bureaucratic checkboxes before you get to the interesting modelling. They are the foundation that determines whether your model is even asking the right question.&lt;br&gt;
The ADF test result determined the entire structure of the ML pipeline. The Jarque-Bera result determined which distribution to fit. The Ljung-Box result determined which lag features to include. The ARCH effect test justified the GARCH modelling. Each test was load-bearing.&lt;br&gt;
If you skip them and go straight to building a model on price levels, you will get results that look plausible and are wrong in ways that are hard to diagnose after the fact. Financial data will make you do the statistics properly — not because a course requires it, but because it will break your model if you don't.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>python</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Calling Your First API Using Its OpenAPI Spec — A Python Walkthrough</title>
      <dc:creator>Iman</dc:creator>
      <pubDate>Fri, 15 May 2026 10:26:02 +0000</pubDate>
      <link>https://forem.com/imann_12/calling-your-first-api-using-its-openapi-spec-a-python-walkthrough-35hj</link>
      <guid>https://forem.com/imann_12/calling-your-first-api-using-its-openapi-spec-a-python-walkthrough-35hj</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on imalaitech.com&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every public API comes with documentation. But some APIs go further — they ship an OpenAPI spec. It's a single JSON file that describes every endpoint, every parameter, and every possible response. Read it once and you know exactly how to use the API before writing a single line of code.&lt;br&gt;
Let's do that right now.&lt;/p&gt;

&lt;p&gt;What Is an OpenAPI Spec?&lt;br&gt;
It's a standardized JSON (or YAML) file that describes an API completely. Endpoints, required parameters, response shapes — all in one place. Tools like Swagger UI turn it into interactive documentation, but the raw file is what matters here.&lt;/p&gt;

&lt;p&gt;Step 1 — Get the Spec&lt;br&gt;
We're using the Swagger Petstore. It's a fake pet store API built purely for learning. Open this in your browser:&lt;br&gt;
&lt;a href="https://petstore3.swagger.io/api/v3/openapi.json" rel="noopener noreferrer"&gt;https://petstore3.swagger.io/api/v3/openapi.json&lt;/a&gt;&lt;br&gt;
Save it locally. Don't try to memorize anything — just scan the structure. You'll notice three things that matter:&lt;/p&gt;

&lt;p&gt;paths — all available endpoints&lt;br&gt;
parameters — what each endpoint accepts&lt;br&gt;
responses — what comes back&lt;/p&gt;

&lt;p&gt;That's the whole mental model.&lt;/p&gt;

&lt;p&gt;Step 2 — Find Your Endpoint&lt;br&gt;
Search for /pet/findByStatus in the spec. Here's what it looks like:&lt;br&gt;
json"/pet/findByStatus": {&lt;br&gt;
  "get": {&lt;br&gt;
    "summary": "Finds Pets by status",&lt;br&gt;
    "parameters": [&lt;br&gt;
      {&lt;br&gt;
        "name": "status",&lt;br&gt;
        "in": "query",&lt;br&gt;
        "required": false,&lt;br&gt;
        "schema": {&lt;br&gt;
          "type": "string",&lt;br&gt;
          "enum": ["available", "pending", "sold"]&lt;br&gt;
        }&lt;br&gt;
      }&lt;br&gt;
    ]&lt;br&gt;
  }&lt;br&gt;
}&lt;br&gt;
The spec tells you everything. One query parameter called status, three allowed values. No guessing.&lt;/p&gt;

&lt;p&gt;Step 3 — Call It in Python&lt;br&gt;
pythonimport requests&lt;/p&gt;

&lt;p&gt;response = requests.get(&lt;br&gt;
    "&lt;a href="https://petstore3.swagger.io/api/v3/pet/findByStatus" rel="noopener noreferrer"&gt;https://petstore3.swagger.io/api/v3/pet/findByStatus&lt;/a&gt;",&lt;br&gt;
    params={"status": "available"}&lt;br&gt;
)&lt;/p&gt;

&lt;p&gt;data = response.json()&lt;br&gt;
print(f"Status: {response.status_code}")&lt;br&gt;
print(f"Pets returned: {len(data)}")&lt;br&gt;
print(data[0])  # peek at the first result&lt;br&gt;
Run it. Then try swapping available for pending or sold and see what changes.&lt;/p&gt;

&lt;p&gt;Step 4 — Read the Response&lt;br&gt;
You'll get back a list of pet objects. Something like:&lt;br&gt;
json{&lt;br&gt;
  "id": 1,&lt;br&gt;
  "name": "doggie",&lt;br&gt;
  "status": "available",&lt;br&gt;
  "photoUrls": []&lt;br&gt;
}&lt;br&gt;
The spec told you this was coming — check the responses section for /pet/findByStatus. It describes the exact shape of what you just received.&lt;/p&gt;

&lt;p&gt;Why This Actually Matters&lt;br&gt;
Most developers go straight to tutorial blog posts when they want to use an API. The spec is better. It's always up to date, it's authoritative, and it tells you things blog posts skip — like which parameters are optional, what the valid enum values are, and what error responses look like.&lt;br&gt;
Get comfortable reading specs now. As your projects grow more complex, this habit saves hours.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>developer</category>
    </item>
    <item>
      <title>Stop Breaking Your System Python: A Practical Guide to Managing Multiple Python Versions</title>
      <dc:creator>Iman</dc:creator>
      <pubDate>Fri, 15 May 2026 10:17:08 +0000</pubDate>
      <link>https://forem.com/imann_12/stop-breaking-your-system-python-a-practical-guide-to-managing-multiple-python-versions-4gfg</link>
      <guid>https://forem.com/imann_12/stop-breaking-your-system-python-a-practical-guide-to-managing-multiple-python-versions-4gfg</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published on imalaitech.com&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every Python developer eventually hits the same wall: one project needs Python 3.9, another requires 3.11, and the new one won't run on anything below 3.12. The instinct is to upgrade globally — which promptly breaks something else.&lt;br&gt;
There's a better way. Two tools worth knowing: pyenv and conda. They solve the same problem differently, and knowing which to reach for matters.&lt;/p&gt;

&lt;p&gt;pyenv — Lightweight, Automatic Version Switching&lt;br&gt;
pyenv lets you install and switch between Python versions at the system, user, or project level. Critically, it never touches your system Python.&lt;br&gt;
Installing pyenv&lt;br&gt;
Linux / Mac:&lt;br&gt;
bashcurl &lt;a href="https://pyenv.run" rel="noopener noreferrer"&gt;https://pyenv.run&lt;/a&gt; | bash&lt;br&gt;
Add this to your ~/.bashrc or ~/.zshrc:&lt;br&gt;
bashexport PYENV_ROOT="$HOME/.pyenv"&lt;br&gt;
export PATH="$PYENV_ROOT/bin:$PATH"&lt;br&gt;
eval "$(pyenv init -)"&lt;br&gt;
Windows — use pyenv-win:&lt;br&gt;
powershellInvoke-WebRequest -UseBasicParsing -Uri "&lt;a href="https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1&lt;/a&gt;" -OutFile "./install-pyenv-win.ps1"; &amp;amp;"./install-pyenv-win.ps1"&lt;br&gt;
Basic Usage&lt;br&gt;
bash# See available versions&lt;br&gt;
pyenv install --list&lt;/p&gt;

&lt;h1&gt;
  
  
  Install a specific version
&lt;/h1&gt;

&lt;p&gt;pyenv install 3.11.9&lt;/p&gt;

&lt;h1&gt;
  
  
  Set global default
&lt;/h1&gt;

&lt;p&gt;pyenv global 3.11.9&lt;/p&gt;

&lt;h1&gt;
  
  
  Set version for current project only
&lt;/h1&gt;

&lt;p&gt;pyenv local 3.10.14&lt;br&gt;
pyenv local creates a .python-version file in your project directory. Every time you cd into that folder, pyenv switches automatically — no manual toggling.&lt;br&gt;
Per-Project Workflow&lt;br&gt;
bashcd my-project&lt;br&gt;
pyenv local 3.10.14&lt;br&gt;
python --version  # Python 3.10.14&lt;/p&gt;

&lt;p&gt;cd ../other-project&lt;br&gt;
pyenv local 3.12.0&lt;br&gt;
python --version  # Python 3.12.0&lt;br&gt;
Clean and automatic.&lt;/p&gt;

&lt;p&gt;conda — Full Environment Management&lt;br&gt;
conda handles Python versions and packages together. It's heavier than pyenv but earns its weight when dependencies get complex — particularly in data science and ML.&lt;br&gt;
Installing conda&lt;br&gt;
Download Miniconda — the minimal install — for Windows, Linux, or Mac.&lt;br&gt;
Basic Usage&lt;br&gt;
bash# Create an environment with a specific Python version&lt;br&gt;
conda create -n myenv python=3.10&lt;/p&gt;

&lt;h1&gt;
  
  
  Activate it
&lt;/h1&gt;

&lt;p&gt;conda activate myenv&lt;/p&gt;

&lt;h1&gt;
  
  
  Install packages
&lt;/h1&gt;

&lt;p&gt;conda install numpy pandas&lt;/p&gt;

&lt;h1&gt;
  
  
  Deactivate
&lt;/h1&gt;

&lt;p&gt;conda deactivate&lt;br&gt;
Per-Project Workflow&lt;br&gt;
bashcd my-project&lt;br&gt;
conda activate project-39    # Python 3.9 environment&lt;/p&gt;

&lt;p&gt;cd ../other-project&lt;br&gt;
conda activate project-312   # Python 3.12 environment&lt;br&gt;
Unlike pyenv, switching isn't automatic — you activate manually. Small trade-off for what you get in return.&lt;/p&gt;

&lt;p&gt;pyenv vs conda — Which One?&lt;br&gt;
pyenvcondaPython version switchingAutomatic (per directory)Manual activationPackage managementNo — use pip + venvYes, built-inBest forGeneral / backend / web devData science / MLWindows supportVia pyenv-win (limited)ExcellentOverheadLightweightHeavier&lt;br&gt;
Use pyenv if you want lightweight, automatic version switching per project.&lt;br&gt;
Use conda if you're in data science, need environment and package management together, or are primarily on Windows.&lt;/p&gt;

&lt;p&gt;The Ideal Setup: pyenv + venv Together&lt;br&gt;
For most developers, this combination covers everything:&lt;br&gt;
bash# Set Python version with pyenv&lt;br&gt;
pyenv local 3.11.9&lt;/p&gt;

&lt;h1&gt;
  
  
  Create a virtual environment
&lt;/h1&gt;

&lt;p&gt;python -m venv .venv&lt;/p&gt;

&lt;h1&gt;
  
  
  Activate it
&lt;/h1&gt;

&lt;p&gt;source .venv/bin/activate      # Linux/Mac&lt;br&gt;
.venv\Scripts\activate         # Windows&lt;/p&gt;

&lt;h1&gt;
  
  
  Install dependencies
&lt;/h1&gt;

&lt;p&gt;pip install -r requirements.txt&lt;br&gt;
pyenv handles the version. venv handles dependency isolation. Best of both.&lt;/p&gt;

&lt;p&gt;Common Issues Worth Knowing&lt;br&gt;
pyenv: command not found after install&lt;br&gt;
Your shell config wasn't reloaded. Run source ~/.bashrc (or ~/.zshrc) after editing it, or close and reopen your terminal.&lt;br&gt;
conda: environment not activating in scripts&lt;br&gt;
Run conda init once after installing, then restart your terminal. This adds the necessary hooks to your shell profile.&lt;br&gt;
python --version still shows system Python after pyenv local&lt;br&gt;
Make sure the pyenv init lines are actually in your shell config and that you've reloaded it. Running pyenv versions should list your installed versions.&lt;/p&gt;

&lt;p&gt;Quick Reference&lt;br&gt;
bash# pyenv&lt;br&gt;
pyenv install 3.11.9&lt;br&gt;
pyenv local 3.11.9       # project-level&lt;br&gt;
pyenv global 3.11.9      # system-level&lt;br&gt;
pyenv versions           # list installed versions&lt;/p&gt;

&lt;h1&gt;
  
  
  conda
&lt;/h1&gt;

&lt;p&gt;conda create -n myenv python=3.11&lt;br&gt;
conda activate myenv&lt;br&gt;
conda deactivate&lt;br&gt;
conda env list           # list all environments&lt;/p&gt;

&lt;p&gt;Managing Python versions correctly is one of those things that saves you hours of debugging later. Set it up once, stop thinking about it.&lt;br&gt;
If you're running into an issue not covered here, drop it in the comments.&lt;/p&gt;

</description>
      <category>python</category>
      <category>linux</category>
      <category>developer</category>
    </item>
  </channel>
</rss>
