feat: house price prediction model integration

This commit is contained in:
nasim 2025-04-03 10:14:54 +02:00
parent 72375db5cd
commit 3aa78be3d6
22 changed files with 251 additions and 24 deletions

View File

@ -0,0 +1,33 @@
from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
import joblib
import pandas as pd
# Load dataset
data = fetch_california_housing()
df = pd.DataFrame(data.data, columns=data.feature_names)
df['target'] = data.target # in 100k USD
# Engineer features
df['square_feet'] = df['AveRooms'] * 350
df['bedrooms'] = df['AveBedrms']
df['bathrooms'] = df['AveRooms'] * 0.2
# Clean bathrooms
df['bathrooms'] = df['bathrooms'].clip(lower=1)
X = df[['square_feet', 'bedrooms', 'bathrooms']]
y = df['target']
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
# Train model
model = LinearRegression()
model.fit(X_train, y_train)
# Need to be tested of course..: )
# Save model
joblib.dump(model, 'price_predictor.pkl')

Binary file not shown.

View File

@ -0,0 +1,34 @@
from pydantic import BaseModel, Field
class HouseCreateRequest(BaseModel):
address: str = Field(
...,
min_length=1,
max_length=255,
description="House address",
examples=["123 Main St"],
)
city: str = Field(
..., description="City where the house is located", examples=["Springfield"]
)
country: str = Field(
..., description="Country where the house is located", examples=["USA"]
)
price: float = Field(..., description="Price of the house", examples=[250000.00])
description: str = Field(
...,
description="Description of the house",
examples=["A beautiful 3-bedroom house"],
)
square_feet: float = Field(
...,
description="Square footage of the house",
examples=[1500.00],
)
bedrooms: int = Field(
..., description="Number of bedrooms in the house", examples=[3]
)
bathrooms: float = Field(
..., description="Number of bathrooms in the house", examples=[2.5]
)

View File

@ -0,0 +1,5 @@
from pydantic import BaseModel
class HouseCreateResponse(BaseModel):
id: str

View File

@ -0,0 +1,8 @@
from pydantic import BaseModel, Field
from typing import Optional
class HouseFeatures(BaseModel):
square_feet: float = Field(..., description="Total square feet of the house")
bedrooms: int = Field(..., description="Number of bedrooms")
bathrooms: float = Field(..., description="Number of bathrooms")
number_of_floors: Optional[int] = Field(default=None, description="Number of floors")

View File

@ -0,0 +1,7 @@
from pydantic import BaseModel
class HousePricePredictionRequest(BaseModel):
square_feet: float
bedrooms: int
bathrooms: float

View File

@ -0,0 +1,5 @@
from pydantic import BaseModel
class HousePricePredictionResponse(BaseModel):
predicted_price: float

View File

@ -0,0 +1,10 @@
from pydantic import BaseModel
class HouseResponse(BaseModel):
id: str
description: str
address: str
city: str
country: str
price: float

View File

@ -0,0 +1,6 @@
from pydantic import BaseModel
from backend.app.dtos.house.house_response import HouseResponse
class HousesListResponse(BaseModel):
houses: list[HouseResponse]

View File

@ -0,0 +1,7 @@
from pydantic import BaseModel
class OwnerDetailResponse(BaseModel):
id: str
user_id: str
email: str

View File

@ -0,0 +1,10 @@
from pydantic import BaseModel
class OwnerResponse(BaseModel):
id: str
user_id: str
class OwnerListResponse(BaseModel):
owners: list[OwnerResponse]

View File

@ -0,0 +1,6 @@
from pydantic import BaseModel
from backend.app.dtos.user.user_response import UserResponse
class UserListResponse(BaseModel):
users: list[UserResponse]

View File

@ -0,0 +1,6 @@
from pydantic import BaseModel
class UserResponse(BaseModel):
id: str
email: str

View File

@ -1,6 +1,11 @@
from typing import Optional, TYPE_CHECKING
from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel
from sqlmodel import Field, Relationship, SQLModel
if TYPE_CHECKING:
from backend.app.models.owner import Owner
class House(SQLModel, table=True):
@ -14,3 +19,4 @@ class House(SQLModel, table=True):
square_feet: float = Field()
bedrooms: int = Field()
bathrooms: float = Field()
owner: Optional["Owner"] = Relationship(back_populates="houses")

View File

@ -1,8 +1,17 @@
from typing import Optional, TYPE_CHECKING
from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel
from sqlmodel import Field, Relationship, SQLModel
if TYPE_CHECKING:
from backend.app.models.house import House
from backend.app.models.user import User
class Owner(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: UUID = Field(foreign_key="user.id", unique=True)
# Relationship
houses: list["House"] = Relationship(back_populates="owner")
user: Optional["User"] = Relationship(back_populates="owner")

View File

@ -1,9 +1,16 @@
from typing import Optional, TYPE_CHECKING
from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel
from sqlmodel import Field, Relationship, SQLModel
if TYPE_CHECKING:
from backend.app.models.owner import Owner
class User(SQLModel, table=True):
id: UUID = Field(default_factory=lambda: uuid4(), primary_key=True)
email: str = Field(unique=True, nullable=False)
password_hash: str = Field(nullable=False)
# Relationships
owner: Optional["Owner"] = Relationship(back_populates="user")

View File

@ -8,7 +8,6 @@ from sqlmodel import asc, desc, select
from ..models.house import House
from ..providers.db_provider import get_session
class HouseRepository:
def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]) -> None:
self.session = session
@ -34,6 +33,11 @@ class HouseRepository:
result = await self.session.execute(statement)
return result.scalar_one_or_none()
async def get_by_user_id(self, user_id: UUID):
statement = select(House).where(House.owner_user_id == user_id)
result = await self.session.execute(statement)
return result.scalars().all()
async def save(self, house: House) -> None:
"""
Save a house to the database. If a house with that ID already exists, do an upsert.

View File

@ -5,6 +5,9 @@ from fastapi import Depends
from sqlalchemy.ext.asyncio.session import AsyncSession
from sqlmodel import select
from backend.app.models.house import House
from backend.app.models.user import User
from ..models.owner import Owner
from ..providers.db_provider import get_session
@ -27,7 +30,26 @@ class OwnerRepository:
statement = select(Owner).where(Owner.user_id == user_id)
result = await self.session.execute(statement)
return result.scalar_one_or_none()
async def get_details_by_house_id(self, house_id: UUID):
statement = (
select(Owner, User)
.join(User, Owner.user_id == User.id)
.join(House, House.owner_user_id == Owner.user_id)
.where(House.id == house_id)
)
result = await self.session.execute(statement)
row = result.first()
if row:
owner, user = row
return {
"owner": owner,
"user": user
}
return None
async def save(self, owner: Owner) -> None:
"""
Save a owner to the database. If an owner with that ID already exists, do an upsert.

View File

@ -2,15 +2,19 @@ from typing import Annotated, Literal
from fastapi import APIRouter, Depends
from ..dtos.house_create_request import HouseCreateRequest
from ..dtos.house_create_response import HouseCreateResponse
from ..dtos.houses_list_response import HouseResponse, HousesListResponse
from backend.app.dtos.house.house_create_request import HouseCreateRequest
from backend.app.dtos.house.house_create_response import HouseCreateResponse
from backend.app.dtos.house.house_features import HouseFeatures
from backend.app.dtos.house.house_predict_request import HousePricePredictionRequest
from backend.app.dtos.house.house_predict_response import HousePricePredictionResponse
from backend.app.dtos.house.house_response import HouseResponse
from backend.app.dtos.house.houses_list_response import HousesListResponse
from ..models.house import House
from ..models.owner import Owner
from ..providers.auth_provider import AuthContext
from ..repositories.house_repository import HouseRepository
from ..repositories.owner_repository import OwnerRepository
from ..services.house_price_predictor import HousePricePredictor
router = APIRouter()
@ -69,4 +73,22 @@ async def get_all_houses(
for house in all_houses
]
return HousesListResponse(houses=house_responses)
@router.post("/predict-price")
async def predict_house_price(
body: HouseFeatures,
price_predictor: Annotated[HousePricePredictor, Depends()],
) -> HousePricePredictionResponse:
"""
Predict the price of a house based on its features.
"""
predicted_price = await price_predictor.predict_california(
square_feet=body.square_feet,
bedrooms=body.bedrooms,
bathrooms=body.bathrooms
)
return HousePricePredictionResponse(predicted_price=predicted_price)

View File

@ -1,15 +1,14 @@
from typing import Annotated
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException
from ..dtos.owner_detail_response import OwnerDetailResponse
from ..dtos.owner_list_response import OwnerListResponse, OwnerResponse
from ..dtos.owner.owner_detail_response import OwnerDetailResponse
from ..dtos.owner.owner_list_response import OwnerListResponse, OwnerResponse
from ..repositories.owner_repository import OwnerRepository
from ..repositories.user_repository import UserRepository
router = APIRouter()
@router.get("")
async def get_owners(
owner_repository: Annotated[OwnerRepository, Depends()],
@ -22,7 +21,6 @@ async def get_owners(
return OwnerListResponse(owners=owners_response)
@router.get("/{id}")
async def get_owner(
id: str,
@ -35,3 +33,22 @@ async def get_owner(
return OwnerDetailResponse(
id=str(owner.id), user_id=str(owner.user_id), email=user.email
)
@router.get("/byhouse/{house_id}")
async def get_owner_by_house_id(
house_id: str,
owner_repository: Annotated[OwnerRepository, Depends()],
) -> OwnerDetailResponse:
result = await owner_repository.get_details_by_house_id(house_id)
if result is None:
raise HTTPException(status_code=404, detail="House or owner not found")
owner = result["owner"]
user = result["user"]
return OwnerDetailResponse(
id=str(owner.id),
user_id=str(owner.user_id),
email=str(user.email)
)

View File

@ -1,16 +1,17 @@
import os
import joblib
import numpy as np
from backend.app.dtos.house.house_features import HouseFeatures
class HousePricePredictor:
"""
Mock ML model that predicts house prices.
In a real scenario, this would load a trained model.
"""
async def predict(
self, square_feet: float, bedrooms: int, bathrooms: float
) -> float:
base_price = square_feet * 200
bedroom_value = bedrooms * 25000
bathroom_value = bathrooms * 15000
predicted_price = base_price + bedroom_value + bathroom_value
return predicted_price
def __init__(self):
self.model = joblib.load("backend/app/ai_models/price_predictor.pkl")
def predict(self, features: HouseFeatures) -> float:
X = np.array([[features.square_feet, features.bedrooms, features.bathrooms]])
return self.model.predict(X)[0] * 100000

View File

@ -8,3 +8,5 @@ python-dotenv
pg8000
asyncpg
greenlet
botbuilder-core
openai