<?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: Nancy</title>
    <description>The latest articles on DEV Community by Nancy (@blockchainlab).</description>
    <link>https://dev.to/blockchainlab</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%2F4054314%2Fde2ce411-a87b-4370-ae01-4c6bfcca774d.jpg</url>
      <title>DEV Community: Nancy</title>
      <link>https://dev.to/blockchainlab</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blockchainlab"/>
    <language>en</language>
    <item>
      <title>Linear Regression: From Least Squares to Production-Ready Practice</title>
      <dc:creator>Nancy</dc:creator>
      <pubDate>Sat, 01 Aug 2026 03:00:26 +0000</pubDate>
      <link>https://dev.to/blockchainlab/linear-regression-from-least-squares-to-production-ready-practice-3b2i</link>
      <guid>https://dev.to/blockchainlab/linear-regression-from-least-squares-to-production-ready-practice-3b2i</guid>
      <description>&lt;h1&gt;
  
  
  Linear Regression: From Least Squares to Production-Ready Practice
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: &lt;code&gt;machinelearning&lt;/code&gt;, &lt;code&gt;datascience&lt;/code&gt;, &lt;code&gt;python&lt;/code&gt;, &lt;code&gt;tutorial&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Linear regression is the first algorithm most people learn, and the one most people never study deeply. It is also the model you will still find in production after fancier algorithms fail, because it is fast, stable, and explainable.&lt;/p&gt;

&lt;p&gt;This article is not a "call &lt;code&gt;.fit()&lt;/code&gt; and read the score" tutorial. We will cover the math, the statistical assumptions, the diagnostics, regularization, evaluation, production concerns, and the interview questions that separate beginners from engineers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Linear Regression Deserves a Second Look
&lt;/h2&gt;

&lt;p&gt;Linear regression is the foundation for understanding almost every other supervised model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logistic regression is linear regression with a sigmoid on top.&lt;/li&gt;
&lt;li&gt;Ridge and Lasso are linear regression with constrained weights.&lt;/li&gt;
&lt;li&gt;Neural networks are stacked linear transformations with nonlinear activations.&lt;/li&gt;
&lt;li&gt;Tree models are judged against the same baseline: "can I beat a linear model?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, linear regression is still the right answer in many business problems. When you need to explain a prediction to a regulator, a client, or a finance team, a clean linear model with interpretable coefficients beats a black box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Math: Least Squares and the Normal Equation
&lt;/h2&gt;

&lt;p&gt;Given features &lt;code&gt;X&lt;/code&gt; and target &lt;code&gt;y&lt;/code&gt;, a linear model assumes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;y = X * beta + epsilon
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is to minimize the residual sum of squares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;L(beta) = ||y - X*beta||^2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Taking the derivative with respect to &lt;code&gt;beta&lt;/code&gt; and setting it to zero gives the &lt;strong&gt;normal equation&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;beta = (X^T * X)^(-1) * X^T * y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, use the pseudoinverse (&lt;code&gt;pinv&lt;/code&gt;) instead of the inverse, because &lt;code&gt;X^T X&lt;/code&gt; may be singular or numerically unstable when features are collinear.&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;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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normal_equation&lt;/span&gt;&lt;span class="p"&gt;(&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;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;Xb&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="n"&gt;c_&lt;/span&gt;&lt;span class="p"&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;ones&lt;/span&gt;&lt;span class="p"&gt;(&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;shape&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="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# add intercept
&lt;/span&gt;    &lt;span class="n"&gt;beta&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="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pinv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Xb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;Xb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;Xb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three Equivalent Views of Least Squares
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Geometric view
&lt;/h3&gt;

&lt;p&gt;The prediction &lt;code&gt;X * beta&lt;/code&gt; is a projection of &lt;code&gt;y&lt;/code&gt; onto the column space of &lt;code&gt;X&lt;/code&gt;. Least squares finds the closest point in that subspace. This is why residuals are orthogonal to the fitted values.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Maximum likelihood view
&lt;/h3&gt;

&lt;p&gt;Assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;epsilon ~ N(0, sigma^2)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then maximizing the log-likelihood is equivalent to minimizing the sum of squared errors. This connection explains why normality of residuals matters for confidence intervals, even though OLS coefficients are still consistent under milder assumptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Optimization view
&lt;/h3&gt;

&lt;p&gt;For large datasets, computing &lt;code&gt;(X^T X)^(-1)&lt;/code&gt; becomes expensive. Gradient descent solves the same objective iteratively:&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;gradient_descent&lt;/span&gt;&lt;span class="p"&gt;(&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;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&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="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;Xb&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="n"&gt;c_&lt;/span&gt;&lt;span class="p"&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;ones&lt;/span&gt;&lt;span class="p"&gt;(&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;shape&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="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Xb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;
    &lt;span class="n"&gt;beta&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;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;grad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Xb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Xb&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;grad&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal equation: exact, best for small and medium datasets. Gradient descent: scalable, but needs feature scaling and learning-rate tuning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumptions You Must Check
&lt;/h2&gt;

&lt;p&gt;A fitted model with a high R-squared is not automatically trustworthy. These assumptions are what make the coefficients interpretable:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assumption&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;How to check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linearity&lt;/td&gt;
&lt;td&gt;Linear relationship between features and target&lt;/td&gt;
&lt;td&gt;residual vs fitted plot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Independence&lt;/td&gt;
&lt;td&gt;Errors are not correlated&lt;/td&gt;
&lt;td&gt;Durbin-Watson statistic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Homoscedasticity&lt;/td&gt;
&lt;td&gt;Constant error variance&lt;/td&gt;
&lt;td&gt;Breusch-Pagan test, residual plot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normality&lt;/td&gt;
&lt;td&gt;Errors are normally distributed&lt;/td&gt;
&lt;td&gt;Q-Q plot, Jarque-Bera test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No multicollinearity&lt;/td&gt;
&lt;td&gt;Features are not highly correlated&lt;/td&gt;
&lt;td&gt;VIF, correlation matrix&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Complete diagnostic walkthrough
&lt;/h3&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;statsmodels.api&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statsmodels.stats.stattools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;durbin_watson&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statsmodels.stats.diagnostic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;het_breuschpagan&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;statsmodels.stats.outliers_influence&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;variance_inflation_factor&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scipy&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;X_const&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_constant&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OLS&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="n"&gt;X_const&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;resid&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;resid&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Durbin-Watson:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;durbin_watson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resid&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="c1"&gt;# Values near 2 mean no autocorrelation; near 0 or 4 is a warning.
&lt;/span&gt;
&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lm_pvalue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fvalue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_pvalue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;het_breuschpagan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_const&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Breusch-Pagan p-value:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_pvalue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# p &amp;gt; 0.05: no strong evidence of heteroscedasticity.
&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Jarque-Bera p-value:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jarque_bera&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resid&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;pvalue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# p &amp;gt; 0.05: no strong evidence against normality.
&lt;/span&gt;
&lt;span class="n"&gt;vif&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DataFrame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;feature&lt;/span&gt;&lt;span class="sh"&gt;"&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;columns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VIF&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="nf"&gt;variance_inflation_factor&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;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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;shape&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="p"&gt;})&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;vif&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# VIF &amp;gt; 10 is commonly treated as a multicollinearity warning.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Regularization: The Bias-Variance Tradeoff
&lt;/h2&gt;

&lt;p&gt;Plain least squares minimizes training error and can overfit when features are many or collinear. Regularization adds a penalty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ridge:       L = MSE + alpha * sum(beta_i^2)
Lasso:       L = MSE + alpha * sum(|beta_i|)
ElasticNet:  L = MSE + alpha * (r * L1 + (1 - r) * L2)
&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;Model&lt;/th&gt;
&lt;th&gt;Penalty&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ridge&lt;/td&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;Shrinks weights, keeps all features&lt;/td&gt;
&lt;td&gt;Many correlated features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lasso&lt;/td&gt;
&lt;td&gt;L1&lt;/td&gt;
&lt;td&gt;Shrinks some weights to exactly zero&lt;/td&gt;
&lt;td&gt;Feature selection needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ElasticNet&lt;/td&gt;
&lt;td&gt;L1 + L2&lt;/td&gt;
&lt;td&gt;Sparse but stable with correlated groups&lt;/td&gt;
&lt;td&gt;Mix of both needs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Critical detail&lt;/strong&gt;: always standardize features before regularization. Otherwise the penalty unfairly shrinks large-magnitude features.&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;sklearn.pipeline&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_pipeline&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.preprocessing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StandardScaler&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.linear_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LinearRegression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Ridge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Lasso&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ElasticNet&lt;/span&gt;

&lt;span class="n"&gt;models&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;OLS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;LinearRegression&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ridge(alpha=1)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Ridge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Lasso(alpha=0.01)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Lasso&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ElasticNet(alpha=0.01)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ElasticNet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&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="n"&gt;l1_ratio&lt;/span&gt;&lt;span class="o"&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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StandardScaler&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;pipe&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="n"&gt;y_pred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipe&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; RMSE=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;mean_squared_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;squared&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;R2=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&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;h2&gt;
  
  
  Evaluation Metrics
&lt;/h2&gt;

&lt;p&gt;Do not report only R-squared. Each metric tells a different story:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Formula meaning&lt;/th&gt;
&lt;th&gt;Use when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MAE&lt;/td&gt;
&lt;td&gt;Mean absolute error&lt;/td&gt;
&lt;td&gt;Errors should not be squared, outliers matter less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RMSE&lt;/td&gt;
&lt;td&gt;Root mean squared error&lt;/td&gt;
&lt;td&gt;Large errors are especially bad&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MAPE&lt;/td&gt;
&lt;td&gt;Mean absolute percentage error&lt;/td&gt;
&lt;td&gt;Business wants a percentage interpretation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R²&lt;/td&gt;
&lt;td&gt;Proportion of variance explained&lt;/td&gt;
&lt;td&gt;Comparing model quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adjusted R²&lt;/td&gt;
&lt;td&gt;R² penalized by feature count&lt;/td&gt;
&lt;td&gt;Comparing models with different feature sets&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Always compare against a &lt;strong&gt;mean baseline&lt;/strong&gt;. If your model only beats "predict the average" by a little, the features are not adding much.&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;baseline_pred&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;full_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&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="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Baseline RMSE:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;mean_squared_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;squared&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Complete End-to-End Example
&lt;/h2&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;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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.datasets&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;fetch_california_housing&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.model_selection&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;train_test_split&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.metrics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mean_squared_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean_absolute_error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r2_score&lt;/span&gt;

&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_california_housing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;as_frame&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="n"&gt;X&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;columns&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;MedHouseVal&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&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MedHouseVal&lt;/span&gt;&lt;span class="sh"&gt;"&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;X_test&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="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;train_test_split&lt;/span&gt;&lt;span class="p"&gt;(&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;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;test_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;random_state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# 1. Handcrafted normal equation
&lt;/span&gt;&lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normal_equation&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;values&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="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;X_test_b&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="n"&gt;c_&lt;/span&gt;&lt;span class="p"&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;ones&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;span class="n"&gt;shape&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="n"&gt;X_test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;y_pred_manual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X_test_b&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;

&lt;span class="c1"&gt;# 2. sklearn baseline
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sklearn.linear_model&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LinearRegression&lt;/span&gt;
&lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LinearRegression&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="n"&gt;y_pred_sklearn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lr&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;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manual&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_pred_manual&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;sklearn&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_pred_sklearn&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: RMSE=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;mean_squared_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;squared&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MAE=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;mean_absolute_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;R2=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;r2_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&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;p&gt;The manual solution and the sklearn solution should produce nearly identical numbers. If they do not, your gradient descent is not converged or your features are not scaled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Feature scaling&lt;/strong&gt;: required for gradient descent and regularized models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-validation&lt;/strong&gt;: never tune alpha on the test set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data leakage&lt;/strong&gt;: split before scaling, encoding, or imputation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: track feature distributions and prediction drift after deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explainability&lt;/strong&gt;: record coefficients, feature importance, and SHAP values for audits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity first&lt;/strong&gt;: start with linear regression before reaching for XGBoost.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Correlation is not causation
&lt;/h3&gt;

&lt;p&gt;A high coefficient does not mean changing the feature will change the target. Omitted variables and reverse causality are always possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Extrapolation
&lt;/h3&gt;

&lt;p&gt;Linear models are dangerous outside the training range. A model trained on houses up to 300 sqm will not tell you what a 3000 sqm house costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. High R-squared does not mean good prediction
&lt;/h3&gt;

&lt;p&gt;R-squared measures in-sample fit. A model can have high R-squared on training data and terrible RMSE on unseen data.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Ignoring residuals
&lt;/h3&gt;

&lt;p&gt;If residuals have a pattern, your model is missing structure. Do not "fix" a nonlinear pattern by adding more linear features blindly.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Using raw features with regularization
&lt;/h3&gt;

&lt;p&gt;Without standardization, Ridge and Lasso apply unequal penalties to features measured in different units.&lt;/p&gt;

&lt;h2&gt;
  
  
  Interview Questions to Be Ready For
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;What is the difference between the normal equation and gradient descent?&lt;/li&gt;
&lt;li&gt;Why is OLS equivalent to maximum likelihood under a normal error assumption?&lt;/li&gt;
&lt;li&gt;What happens if &lt;code&gt;X^T X&lt;/code&gt; is singular, and how does Ridge help?&lt;/li&gt;
&lt;li&gt;How do you detect and fix multicollinearity?&lt;/li&gt;
&lt;li&gt;Why use adjusted R-squared instead of R-squared?&lt;/li&gt;
&lt;li&gt;What does a residual vs fitted plot tell you?&lt;/li&gt;
&lt;li&gt;When would you prefer Lasso over Ridge?&lt;/li&gt;
&lt;li&gt;Why must you standardize before L1 or L2 regularization?&lt;/li&gt;
&lt;li&gt;What are the consequences of heteroscedasticity?&lt;/li&gt;
&lt;li&gt;How do you detect data leakage in a regression pipeline?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Linear regression looks simple, but it contains the core ideas of machine learning: objective functions, optimization, statistical assumptions, regularization, evaluation, and deployment. If you deeply understand this one model, every algorithm after it becomes easier to learn.&lt;/p&gt;

&lt;p&gt;Start with the math, validate the assumptions, and keep the production concerns in mind. The model that is "too simple" is often the one that survives in production the longest.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Follow me for more practical AI and data engineering posts.&lt;/em&gt;&lt;br&gt;
没    ；。l&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build a RAG-Powered Database Assistant with PostgreSQL and pgvector</title>
      <dc:creator>Nancy</dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:01:59 +0000</pubDate>
      <link>https://dev.to/blockchainlab/build-a-rag-powered-database-assistant-with-postgresql-and-pgvector-328i</link>
      <guid>https://dev.to/blockchainlab/build-a-rag-powered-database-assistant-with-postgresql-and-pgvector-328i</guid>
      <description>&lt;p&gt;Build a RAG-Powered Database Assistant with PostgreSQL and pgvector&lt;br&gt;
Tags: ai, database, postgres, tutorial&lt;/p&gt;

&lt;p&gt;The Problem&lt;br&gt;
Every company has the same conversation with their database:&lt;/p&gt;

&lt;p&gt;"How many orders did we lose in the last 30 days?" "Which customers ordered more than three times but never left a review?" "Show me the revenue trend by region for the past quarter."&lt;/p&gt;

&lt;p&gt;These questions are simpleblog_post_en.mdblog_post_en.md for a human analyst, but they never make it into a SQL query fast enough. Business teams wait for engineering. Engineering is busy. The query finally gets written three days later, and the answer is already stale.&lt;/p&gt;

&lt;p&gt;What if the database itself could answer natural-language questions in seconds?&lt;/p&gt;

&lt;p&gt;That is exactly what this tutorial builds: a RAG-powered database assistant that retrieves relevant schema context, generates SQL with an LLM, and returns the answer directly.&lt;/p&gt;

&lt;p&gt;Why RAG instead of "just ask ChatGPT"?&lt;/p&gt;

&lt;p&gt;Schema-aware: the LLM sees the actual tables, columns, and relationships in your database.&lt;br&gt;
Up-to-date: metadata changes are reflected immediately without retraining.&lt;br&gt;
Safe: the generated SQL runs on a read-only connection, so it cannot mutate data.&lt;br&gt;
Explainable: users can see which schema context was used to answer the question.&lt;br&gt;
Why PostgreSQL + pgvector&lt;br&gt;
PostgreSQL is the most versatile production database in use today. It already supports JSON, full-text search, window functions, and rich indexes. Since 2023, the pgvector extension gives it production-grade vector search without adding another database to your stack.&lt;/p&gt;

&lt;p&gt;Using the same database for both operational data and retrieval means:&lt;/p&gt;

&lt;p&gt;One backup strategy, one monitoring stack, one set of credentials.&lt;br&gt;
SQL joins between metadata and your real tables.&lt;br&gt;
No extra network hop to a separate vector store.&lt;br&gt;
Architecture&lt;br&gt;
User question&lt;br&gt;
       |&lt;br&gt;
       v&lt;br&gt;
[1] Embed question (text-embedding-3-small)&lt;br&gt;
       |&lt;br&gt;
       v&lt;br&gt;
[2] pgvector similarity search over schema_metadata&lt;br&gt;
       |&lt;br&gt;
       v&lt;br&gt;
[3] Build prompt: question + relevant tables/columns/examples&lt;br&gt;
       |&lt;br&gt;
       v&lt;br&gt;
[4] LLM generates SQL (read-only)&lt;br&gt;
       |&lt;br&gt;
       v&lt;br&gt;
[5] Execute SQL -&amp;gt; return answer + explanation&lt;br&gt;
The key idea is that we are not storing documents; we are storing database schema metadata as the retrieval corpus. That is the difference between a generic chatbot and a database assistant.&lt;/p&gt;

&lt;p&gt;Project Setup&lt;br&gt;
Prerequisites&lt;br&gt;
PostgreSQL 15+ with pgvector installed&lt;br&gt;
Python 3.11+&lt;br&gt;
An OpenAI-compatible API key&lt;br&gt;
pip install "psycopg[binary]" openai&lt;br&gt;
import os&lt;br&gt;
import json&lt;br&gt;
import psycopg&lt;br&gt;
from openai import OpenAI&lt;/p&gt;

&lt;p&gt;client = OpenAI()  # reads OPENAI_API_KEY from environment&lt;/p&gt;

&lt;p&gt;DB_URL = os.getenv(&lt;br&gt;
    "DATABASE_URL",&lt;br&gt;
    "postgresql://postgres:postgres@localhost:5432/rag_db",&lt;br&gt;
)&lt;br&gt;
Step 1: Enable pgvector and Create the Metadata Table&lt;br&gt;
with psycopg.connect(DB_URL, autocommit=True) as conn:&lt;br&gt;
    conn.execute("CREATE EXTENSION IF NOT EXISTS vector")&lt;br&gt;
    conn.execute("""&lt;br&gt;
        CREATE TABLE IF NOT EXISTS schema_metadata (&lt;br&gt;
            id            BIGSERIAL PRIMARY KEY,&lt;br&gt;
            object_name   TEXT NOT NULL,&lt;br&gt;
            object_type   TEXT NOT NULL,       -- table | column | example&lt;br&gt;
            definition    TEXT NOT NULL,       -- DDL or column description&lt;br&gt;
            description   TEXT NOT NULL,       -- natural-language semantics&lt;br&gt;
            embedding     vector(1536)         -- text-embedding-3-small&lt;br&gt;
        );&lt;br&gt;
    """)&lt;br&gt;
    conn.execute("""&lt;br&gt;
        CREATE INDEX IF NOT EXISTS schema_metadata_embedding_idx&lt;br&gt;
        ON schema_metadata&lt;br&gt;
        USING hnsw (embedding vector_cosine_ops);&lt;br&gt;
    """)&lt;br&gt;
The HNSW index gives approximate nearest-neighbor search with sub-10ms latency on modest datasets. For a few thousand metadata rows, this is effectively instant.&lt;/p&gt;

&lt;p&gt;Step 2: Extract Schema Metadata from the Database&lt;br&gt;
Instead of manually writing table descriptions, we extract the real schema from information_schema:&lt;/p&gt;

&lt;p&gt;def fetch_schema_metadata(conn) -&amp;gt; list[dict]:&lt;br&gt;
    rows = conn.execute("""&lt;br&gt;
        SELECT&lt;br&gt;
            c.table_name,&lt;br&gt;
            c.column_name,&lt;br&gt;
            c.data_type,&lt;br&gt;
            pg_catalog.col_description(&lt;br&gt;
                ('"' || c.table_schema || '"."' || c.table_name || '"')::regclass,&lt;br&gt;
                c.ordinal_position&lt;br&gt;
            ) AS column_comment&lt;br&gt;
        FROM information_schema.columns c&lt;br&gt;
        WHERE c.table_schema = 'public'&lt;br&gt;
        ORDER BY c.table_name, c.ordinal_position&lt;br&gt;
    """).fetchall()&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metadata = []
for table_name, column_name, data_type, comment in rows:
    metadata.append({
        "object_name": f"{table_name}.{column_name}",
        "object_type": "column",
        "definition": (
            f"Column: {column_name}\n"
            f"Type: {data_type}\n"
            f"Table: {table_name}"
        ),
        "description": comment or f"{column_name} column in {table_name} table",
    })
return metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;We also add query examples. These teach the LLM the query style used by your team:&lt;/p&gt;

&lt;p&gt;QUERY_EXAMPLES = [&lt;br&gt;
    {&lt;br&gt;
        "object_name": "example:monthly_revenue",&lt;br&gt;
        "object_type": "example",&lt;br&gt;
        "definition": (&lt;br&gt;
            "Question: What is the monthly revenue?\n"&lt;br&gt;
            "SQL: SELECT DATE_TRUNC('month', created_at) AS month, "&lt;br&gt;
            "SUM(total_amount) FROM orders GROUP BY 1 ORDER BY 1;"&lt;br&gt;
        ),&lt;br&gt;
        "description": "Monthly revenue aggregation on orders",&lt;br&gt;
    },&lt;br&gt;
    {&lt;br&gt;
        "object_name": "example:top_customers",&lt;br&gt;
        "object_type": "example",&lt;br&gt;
        "definition": (&lt;br&gt;
            "Question: Who are the top 10 customers by spend?\n"&lt;br&gt;
            "SQL: SELECT c.name, SUM(o.total_amount) AS spend "&lt;br&gt;
            "FROM customers c JOIN orders o ON o.customer_id = c.id "&lt;br&gt;
            "GROUP BY c.id ORDER BY spend DESC LIMIT 10;"&lt;br&gt;
        ),&lt;br&gt;
        "description": "Top customers by total order amount",&lt;br&gt;
    },&lt;br&gt;
]&lt;br&gt;
Step 3: Embed and Store Metadata&lt;br&gt;
def embed_texts(texts: list[str]) -&amp;gt; list[list[float]]:&lt;br&gt;
    resp = client.embeddings.create(&lt;br&gt;
        model="text-embedding-3-small",&lt;br&gt;
        input=texts,&lt;br&gt;
    )&lt;br&gt;
    return [item.embedding for item in resp.data]&lt;/p&gt;

&lt;p&gt;def ingest_metadata():&lt;br&gt;
    with psycopg.connect(DB_URL) as conn:&lt;br&gt;
        schema_metadata = fetch_schema_metadata(conn)&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs = []
for item in schema_metadata + QUERY_EXAMPLES:
    docs.append(f"{item['object_name']}\n{item['definition']}\n{item['description']}")

vectors = embed_texts(docs)

with psycopg.connect(DB_URL) as conn:
    conn.execute("TRUNCATE schema_metadata")
    for item, vector in zip(schema_metadata + QUERY_EXAMPLES, vectors):
        conn.execute("""
            INSERT INTO schema_metadata
                (object_name, object_type, definition, description, embedding)
            VALUES (%s, %s, %s, %s, %s)
        """, (
            item["object_name"],
            item["object_type"],
            item["definition"],
            item["description"],
            vector,
        ))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Note: vector accepts a Python list directly through psycopg; the extension serializes it to its native format.&lt;/p&gt;

&lt;p&gt;Step 4: Retrieve Relevant Schema Context&lt;br&gt;
def retrieve_context(question: str, top_k: int = 5) -&amp;gt; str:&lt;br&gt;
    q_vector = embed_texts([question])[0]&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;with psycopg.connect(DB_URL) as conn:
    rows = conn.execute("""
        SELECT object_name, object_type, definition, description,
               1 - (embedding &amp;lt;=&amp;gt; %s::vector) AS similarity
        FROM schema_metadata
        ORDER BY embedding &amp;lt;=&amp;gt; %s::vector
        LIMIT %s
    """, (q_vector, q_vector, top_k)).fetchall()

context_blocks = []
for name, obj_type, definition, description, sim in rows:
    context_blocks.append(
        f"### {name} ({obj_type}, similarity={sim:.3f})\n"
        f"{definition}\n{description}"
    )
return "\n\n".join(context_blocks)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The cosine operator &amp;lt;=&amp;gt; is the key line. It ranks schema fragments by semantic relevance to the user's question.&lt;/p&gt;

&lt;p&gt;Step 5: Generate SQL and Return an Answer&lt;br&gt;
SYSTEM_PROMPT = """&lt;br&gt;
You are a senior database analyst. Your job is to convert&lt;br&gt;
natural-language questions into safe, correct PostgreSQL.&lt;/p&gt;

&lt;p&gt;Rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use only tables and columns from the provided context.&lt;/li&gt;
&lt;li&gt;Always wrap identifiers that need it in double quotes.&lt;/li&gt;
&lt;li&gt;Never use DELETE, UPDATE, INSERT, DROP, TRUNCATE, or DDL.&lt;/li&gt;
&lt;li&gt;Add LIMIT unless the user explicitly asks for all rows.&lt;/li&gt;
&lt;li&gt;Return only SQL inside a sql code fence, then one short
explanation in plain text.
"""&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;def generate_sql(question: str, context: str) -&amp;gt; str:&lt;br&gt;
    response = client.chat.completions.create(&lt;br&gt;
        model="gpt-4o-mini",&lt;br&gt;
        messages=[&lt;br&gt;
            {"role": "system", "content": SYSTEM_PROMPT},&lt;br&gt;
            {"role": "user", "content": (&lt;br&gt;
                f"Database schema context:\n{context}\n\n"&lt;br&gt;
                f"Question: {question}\n\n"&lt;br&gt;
                "Generate PostgreSQL."&lt;br&gt;
            )},&lt;br&gt;
        ],&lt;br&gt;
        temperature=0,&lt;br&gt;
    )&lt;br&gt;
    content = response.choices[0].message.content&lt;br&gt;
    return extract_sql(content)&lt;/p&gt;

&lt;p&gt;def extract_sql(content: str) -&amp;gt; str:&lt;br&gt;
    if "&lt;br&gt;
&lt;br&gt;
&lt;code&gt;sql" in content:&lt;br&gt;
        return content.split("&lt;/code&gt;&lt;br&gt;
&lt;br&gt;
sql")[1].split("&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
")[0].strip()
    return content.strip()


def execute_read_only(sql: str) -&amp;gt; list[tuple]:
    # The application user owns the data; the assistant
    # connects with a read-only role in production.
    with psycopg.connect(DB_URL) as conn:
        cur = conn.execute(sql)
        return cur.fetchall()
Putting It All Together
def ask_database(question: str) -&amp;gt; None:
    print(f"Q: {question}\n")

    context = retrieve_context(question)
    print(f"Retrieved context:\n{context}\n")

    sql = generate_sql(question, context)
    print(f"Generated SQL:\n{sql}\n")

    rows = execute_read_only(sql)
    print(f"Result rows: {len(rows)}")
    for row in rows[:10]:
        print(row)


if __name__ == "__main__":
    ask_database("Show monthly revenue for the last six months.")
Example output:

Q: Show monthly revenue for the last six months.

Generated SQL:
SELECT DATE_TRUNC('month', created_at) AS month,
       SUM(total_amount) AS revenue
FROM orders
WHERE created_at &amp;gt;= CURRENT_DATE - INTERVAL '6 months'
GROUP BY 1
ORDER BY 1;

Result rows:
(datetime.date(2026, 2, 1), Decimal('128450.00'))
(datetime.date(2026, 3, 1), Decimal('153900.25'))
...
Production Optimization Tips
1. Add a schema refresh job
Run ingest_metadata() nightly, or hook it into your CI migration pipeline so new columns are searchable the same day they ship.

2. Use a read-only database role
CREATE ROLE assistant_readonly LOGIN PASSWORD '...';
GRANT CONNECT ON DATABASE rag_db TO assistant_readonly;
GRANT USAGE ON SCHEMA public TO assistant_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO assistant_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
    GRANT SELECT ON TABLES TO assistant_readonly;
The application should connect with this role, not the superuser.

3. Cache embeddings
Embedding calls cost time and money. Cache by metadata content hash; re-embed only rows that changed.

4. Add guardrails
Validate the generated SQL with EXPLAIN before execution.
Reject queries containing forbidden keywords as a second safety net.
Log every question, SQL, and result count for auditability.
5. Let the LLM explain the result
After execution, send the row summary back to the LLM and ask for a concise, business-friendly interpretation. This is what makes the tool feel useful to non-technical teams.

When Not to Use This Pattern
Ad-hoc data exploration on massive tables: the LLM will write SELECT * or miss an index. Keep a human in the loop.
Financial reporting with strict audit requirements: use a governed BI tool instead.
Low-latency APIs: LLM inference adds seconds. Pre-generate SQL for known questions or cache results.
Complex domain semantics: if your schema names are cryptic, invest in comments first; retrieval is only as good as the metadata.
Conclusion
This pattern is not about replacing SQL. It is about making the database accessible to people who should not need to learn SQL to get an answer.

The stack is deliberately boring:

PostgreSQL for storage and search
pgvector for vector similarity
OpenAI-compatible embeddings and chat completions
A Python script that ties them together
The result is a database assistant that understands your schema, respects your query style, and answers business questions in seconds. Start with one table and one example query. Add more metadata as you trust it, and your team will stop waiting for engineering to write the next report.

Found this useful? Follow me for more practical AI + database engineering posts.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>ai</category>
      <category>database</category>
      <category>postgres</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
