DEV Community

Cover image for I Built a Multi Linear Regression Model in Python and Exposed it as an API for my Angular App
Placide
Placide

Posted on

I Built a Multi Linear Regression Model in Python and Exposed it as an API for my Angular App

A few weeks ago I was just another developer who knew fullstack development inside out but had trained some few machine learning model in my life.

Today I have a working ML model running in Python, served as a REST API, and consumed live inside an Angular app.

This article is the honest, step-by-step story of how I got there — the code, the mistakes, and the moments where it all clicked. If you're a frontend developer curious about ML, this one is for you.

What We're Building

A house price prediction system with three parts:

┌─────────────────────────────────────────────┐
│              Angular Frontend               │
│  User enters: size, rooms, age, distance    │
│  App displays: predicted price              │
└─────────────────┬───────────────────────────┘
                  │ HTTP POST
┌─────────────────▼───────────────────────────┐
│           Python FastAPI Server             │
│  Receives features → runs prediction        │
│  Returns: predicted price                   │
└─────────────────┬───────────────────────────┘
                  │
┌─────────────────▼───────────────────────────┐
│       Multi Linear Regression Model         │
│  Trained on historical house price data     │
│  Features: size, rooms, age, distance       │
└─────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Part 1 — Understanding the Model

Before writing a single line of code, let me explain what Multi Linear Regression actually does — because understanding it changed how I thought about the whole project.

Simple Linear Regression uses one input to predict one output:
Price = (weight × Size) + base_value

Multi Linear Regression uses multiple inputs to predict one output:
Price = (w1 × Size) + (w2 × Rooms) + (w3 × Age) + (w4 × Distance to city) + base_value
The model's job during training is to figure out the best values for w1, w2, w3, and w4 — the weights that minimize the difference between its predictions and the real prices in your training data.

As a developer I think of it like this: the model is learning a formula. You give it historical examples, it reverse-engineers the formula, and then applies that formula to new data it's never seen.

Part 2 — Building the Python ML Model

Setup

mkdir house-price-ml
cd house-price-ml
python -m venv venv
source venv/bin/activate 
pip install numpy pandas scikit-learn fastapi uvicorn joblib
Enter fullscreen mode Exit fullscreen mode

The Training Data

For this article we'll use a synthetic dataset that mirrors real-world house pricing patterns:

# model/train.py
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, r2_score
from sklearn.preprocessing import StandardScaler
import joblib
import os

# Generate realistic synthetic data
np.random.seed(42)
n_samples = 500

size = np.random.randint(60, 300, n_samples)          
rooms = np.random.randint(1, 7, n_samples)             
age = np.random.randint(0, 40, n_samples)              
distance = np.random.randint(1, 30, n_samples)      

# Realistic price formula + noise
price = (
    1800 * size +
    15000 * rooms -
    2500 * age -
    4000 * distance +
    np.random.normal(0, 20000, n_samples) +
    50000  # base value
)

# Build DataFrame
df = pd.DataFrame({
    'size': size,
    'rooms': rooms,
    'age': age,
    'distance': distance,
    'price': price
})

print("Dataset preview:")
print(df.head())
print(f"\nDataset shape: {df.shape}")
print(f"Price range: ${df['price'].min():,.0f} — ${df['price'].max():,.0f}")
Enter fullscreen mode Exit fullscreen mode

Feature Engineering — Scaling

Remember Feature Engineering? This is where it matters. Our features have very different scales — size goes up to 300, distance up to 30. Without scaling, the model unfairly weights larger numbers.

# Prepare features and target
X = df[['size', 'rooms', 'age', 'distance']].values
y = df['price'].values

# Split into training and test sets
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Scale features — crucial for good predictions
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Enter fullscreen mode Exit fullscreen mode

Training and Evaluating the Model

# Train the model
model = LinearRegression()
model.fit(X_train_scaled, y_train)

# Evaluate on test data
y_pred = model.predict(X_test_scaled)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)

print(f"\nModel Performance:")
print(f"Mean Absolute Error: ${mae:,.0f}")
print(f"R² Score: {r2:.4f}")
print(f"\nFeature weights:")
features = ['size', 'rooms', 'age', 'distance']
for feature, coef in zip(features, model.coef_):
    print(f"  {feature}: {coef:,.2f}")

# Save the model and scaler for the API
os.makedirs('saved_model', exist_ok=True)
joblib.dump(model, 'saved_model/model.pkl')
joblib.dump(scaler, 'saved_model/scaler.pkl')
print("\n Model and scaler saved successfully!")
Enter fullscreen mode Exit fullscreen mode

after running it you should see have this output

Model Performance:
Mean Absolute Error: $18,432
R² Score: 0.9721

Feature weights:
  size: 142,831.24
  rooms: 12,847.63
  age: -19,284.51
  distance: -31,847.22

 Model and scaler saved successfully!
Enter fullscreen mode Exit fullscreen mode

An R² score of 0.97 means the model explains 97% of the variance in house prices. For our synthetic data that's excellent.

Notice the signs on the weights — size and rooms push the price up (positive), age and distance push it down (negative). That matches real-world intuition perfectly.

Part 3 — Building the Python API with FastAPI

Now we wrap the model in a REST API that our Angular app can talk to:

# api/main.py
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, validator
import joblib
import numpy as np
import os

app = FastAPI(
    title="House Price Prediction API",
    description="Multi Linear Regression model served as a REST API",
    version="1.0.0"
)

# CORS — allow Angular dev server
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:4200"],
    allow_methods=["*"],
    allow_headers=["*"]
)

# Load model and scaler at startup
model_path = "saved_model/model.pkl"
scaler_path = "saved_model/scaler.pkl"

if not os.path.exists(model_path):
    raise RuntimeError("Model not found. Run train.py first.")

model = joblib.load(model_path)
scaler = joblib.load(scaler_path)

# Request schema with validation
class HouseFeatures(BaseModel):
    size: float = Field(..., gt=0, le=1000, description="Size in m²")
    rooms: int = Field(..., ge=1, le=20, description="Number of rooms")
    age: int = Field(..., ge=0, le=100, description="Age in years")
    distance: float = Field(..., ge=0, le=100, description="Distance to city in km")

    @validator('size')
    def size_must_be_realistic(cls, v):
        if v < 20:
            raise ValueError('Size must be at least 20m²')
        return v

# Response schema
class PredictionResponse(BaseModel):
    predicted_price: float
    formatted_price: str
    confidence_note: str
    features_used: dict

@app.get("/")
def root():
    return { "status": "online", "model": "Multi Linear Regression", "version": "1.0.0" }

@app.get("/health")
def health():
    return { "status": "healthy", "model_loaded": model is not None }

@app.post("/predict", response_model=PredictionResponse)
def predict(features: HouseFeatures):
    try:
        # Prepare input for the model
        input_data = np.array([[
            features.size,
            features.rooms,
            features.age,
            features.distance
        ]])

        # Scale the input — same scaler used in training
        input_scaled = scaler.transform(input_data)

        # Run prediction
        prediction = model.predict(input_scaled)[0]

        # Clamp to realistic range
        prediction = max(50000, min(prediction, 5000000))

        return PredictionResponse(
            predicted_price=round(prediction, 2),
            formatted_price=f"${prediction:,.0f}",
            confidence_note="Prediction based on 500 historical data points",
            features_used={
                "size_m2": features.size,
                "rooms": features.rooms,
                "age_years": features.age,
                "distance_km": features.distance
            }
        )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
Enter fullscreen mode Exit fullscreen mode

We need to start the API:

uvicorn api.main:app --reload --port 8000
Enter fullscreen mode Exit fullscreen mode

Part 4 — Building the Angular Frontend

The Prediction Service

// src/app/core/services/prediction.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface HouseFeatures {
  size: number;
  rooms: number;
  age: number;
  distance: number;
}

export interface PredictionResponse {
  predicted_price: number;
  formatted_price: string;
  confidence_note: string;
  features_used: Record<string, number>;
}

@Injectable({ providedIn: 'root' })
export class PredictionService {
  private http = inject(HttpClient);
  private apiUrl = 'http://localhost:8000';

  predict(features: HouseFeatures): Observable<PredictionResponse> {
    return this.http.post<PredictionResponse>(
      `${this.apiUrl}/predict`,
      features
    );
  }

  checkHealth(): Observable<{ status: string }> {
    return this.http.get<{ status: string }>(`${this.apiUrl}/health`);
  }
}
Enter fullscreen mode Exit fullscreen mode

The Prediction Component

// src/app/features/predictor/predictor.component.ts
import { Component, inject, signal } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { PredictionService, PredictionResponse } from '../../core/services/prediction.service';

@Component({
  selector: 'app-predictor',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <div class="predictor">
      <div class="predictor__header">
        <h1> House Price Predictor</h1>
        <p>Powered by a Multi Linear Regression model trained in Python</p>
      </div>

      <form [formGroup]="form" (ngSubmit)="predict()" class="predictor__form">
        <div class="form-grid">

          <div class="form-field">
            <label for="size">Size (m²)</label>
            <input
              id="size"
              type="number"
              formControlName="size"
              placeholder="e.g. 120"
            />
            @if (form.get('size')?.invalid && form.get('size')?.touched) {
              <span class="error">Enter a valid size (20–1000 m²)</span>
            }
          </div>

          <div class="form-field">
            <label for="rooms">Number of Rooms</label>
            <input
              id="rooms"
              type="number"
              formControlName="rooms"
              placeholder="e.g. 3"
            />
            @if (form.get('rooms')?.invalid && form.get('rooms')?.touched) {
              <span class="error">Enter a valid number (1–20)</span>
            }
          </div>

          <div class="form-field">
            <label for="age">Age of Property (years)</label>
            <input
              id="age"
              type="number"
              formControlName="age"
              placeholder="e.g. 10"
            />
            @if (form.get('age')?.invalid && form.get('age')?.touched) {
              <span class="error">Enter a valid age (0–100)</span>
            }
          </div>

          <div class="form-field">
            <label for="distance">Distance to City (km)</label>
            <input
              id="distance"
              type="number"
              formControlName="distance"
              placeholder="e.g. 5"
            />
            @if (form.get('distance')?.invalid && form.get('distance')?.touched) {
              <span class="error">Enter a valid distance (0–100 km)</span>
            }
          </div>

        </div>

        <button
          type="submit"
          [disabled]="form.invalid || isLoading()"
          class="predict-btn">
          {{ isLoading() ? 'Predicting...' : 'Predict Price' }}
        </button>
      </form>

      @if (error()) {
        <div class="error-banner">
           {{ error() }}
        </div>
      }

      @if (result()) {
        <div class="result">
          <div class="result__price">
            {{ result()!.formatted_price }}
          </div>
          <p class="result__note">{{ result()!.confidence_note }}</p>

          <div class="result__breakdown">
            <h3>Features used:</h3>
            <ul>
              <li> Size: {{ result()!.features_used['size_m2'] }} m²</li>
              <li> Rooms: {{ result()!.features_used['rooms'] }}</li>
              <li> Age: {{ result()!.features_used['age_years'] }} years</li>
              <li> Distance: {{ result()!.features_used['distance_km'] }} km</li>
            </ul>
          </div>
        </div>
      }
    </div>
  `
})
export class PredictorComponent {
  private fb = inject(FormBuilder);
  private predictionService = inject(PredictionService);

  isLoading = signal(false);
  result = signal<PredictionResponse | null>(null);
  error = signal<string | null>(null);

  form = this.fb.group({
    size: [null, [Validators.required, Validators.min(20), Validators.max(1000)]],
    rooms: [null, [Validators.required, Validators.min(1), Validators.max(20)]],
    age: [null, [Validators.required, Validators.min(0), Validators.max(100)]],
    distance: [null, [Validators.required, Validators.min(0), Validators.max(100)]]
  });

  predict(): void {
    if (this.form.invalid) return;

    this.isLoading.set(true);
    this.error.set(null);
    this.result.set(null);

    this.predictionService.predict(this.form.value as any).subscribe({
      next: response => {
        this.result.set(response);
        this.isLoading.set(false);
      },
      error: err => {
        this.error.set('Could not reach the prediction API. Is the Python server running?');
        this.isLoading.set(false);
      }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

The Full Project Structure

house-price-predictor/
├──  Python (Backend)
│   ├── model/
│   │   └── train.py          
│   ├── api/
│   │   └── main.py           
│   └── saved_model/
│       ├── model.pkl         
│       └── scaler.pkl        
│
└──  Angular (Frontend)
    └── src/app/
        ├── core/services/
        │   └── prediction.service.ts
        └── features/predictor/
            └── predictor.component.ts
Enter fullscreen mode Exit fullscreen mode

Running the Full Stack

Terminal 1 — Python API:

cd house-price-predictor
source venv/bin/activate
python model/train.py       
uvicorn api.main:app --reload --port 8000
Enter fullscreen mode Exit fullscreen mode

Terminal 2 — Angular:

ng serve
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:4200, enter house features, and watch your Angular app talk to your ML model in real time.

What I Learned

ML and frontend are closer than I thought

The hardest part wasn't the ML — it was connecting the two worlds. Once I understood that the model is just a function that takes numbers and returns a number, everything clicked.

Feature scaling is non-negotiable

This model without StandardScaler gave terrible predictions. Five minutes of preprocessing made all the difference. This connects directly back to Feature Engineering — garbage in, garbage out.

FastAPI is a frontend developer's dream

Auto-generated docs, type validation, clean JSON responses — FastAPI thinks the same way Angular does. Strong types everywhere.

The weights tell a story

Looking at the model's coefficients — seeing that age has a negative weight and size has a positive one — made the math feel real. The model learned something true about the world from data alone.

Conclusion

Six weeks ago I didn't know what a regression model was. Today I have one running in production serving predictions to an Angular frontend.

The gap between fullstack development and machine learning is smaller than it looks — especially if you already think in terms of functions, inputs, outputs, and APIs.

This is what learning in public looks like. Messy, exciting, and worth every confused moment.

Are you a frontend or fullstack developer exploring ML? What's your biggest question right now? let's figure it out together!

Top comments (0)