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 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): class House(SQLModel, table=True):
@ -14,3 +19,4 @@ class House(SQLModel, table=True):
square_feet: float = Field() square_feet: float = Field()
bedrooms: int = Field() bedrooms: int = Field()
bathrooms: float = 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 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): class Owner(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True) id: UUID = Field(default_factory=uuid4, primary_key=True)
user_id: UUID = Field(foreign_key="user.id", unique=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 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): class User(SQLModel, table=True):
id: UUID = Field(default_factory=lambda: uuid4(), primary_key=True) id: UUID = Field(default_factory=lambda: uuid4(), primary_key=True)
email: str = Field(unique=True, nullable=False) email: str = Field(unique=True, nullable=False)
password_hash: str = Field(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 ..models.house import House
from ..providers.db_provider import get_session from ..providers.db_provider import get_session
class HouseRepository: class HouseRepository:
def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]) -> None: def __init__(self, session: Annotated[AsyncSession, Depends(get_session)]) -> None:
self.session = session self.session = session
@ -34,6 +33,11 @@ class HouseRepository:
result = await self.session.execute(statement) result = await self.session.execute(statement)
return result.scalar_one_or_none() 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: async def save(self, house: House) -> None:
""" """
Save a house to the database. If a house with that ID already exists, do an upsert. 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 sqlalchemy.ext.asyncio.session import AsyncSession
from sqlmodel import select from sqlmodel import select
from backend.app.models.house import House
from backend.app.models.user import User
from ..models.owner import Owner from ..models.owner import Owner
from ..providers.db_provider import get_session from ..providers.db_provider import get_session
@ -27,7 +30,26 @@ class OwnerRepository:
statement = select(Owner).where(Owner.user_id == user_id) statement = select(Owner).where(Owner.user_id == user_id)
result = await self.session.execute(statement) result = await self.session.execute(statement)
return result.scalar_one_or_none() 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: async def save(self, owner: Owner) -> None:
""" """
Save a owner to the database. If an owner with that ID already exists, do an upsert. 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 fastapi import APIRouter, Depends
from ..dtos.house_create_request import HouseCreateRequest from backend.app.dtos.house.house_create_request import HouseCreateRequest
from ..dtos.house_create_response import HouseCreateResponse from backend.app.dtos.house.house_create_response import HouseCreateResponse
from ..dtos.houses_list_response import HouseResponse, HousesListResponse 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.house import House
from ..models.owner import Owner from ..models.owner import Owner
from ..providers.auth_provider import AuthContext from ..providers.auth_provider import AuthContext
from ..repositories.house_repository import HouseRepository from ..repositories.house_repository import HouseRepository
from ..repositories.owner_repository import OwnerRepository from ..repositories.owner_repository import OwnerRepository
from ..services.house_price_predictor import HousePricePredictor
router = APIRouter() router = APIRouter()
@ -69,4 +73,22 @@ async def get_all_houses(
for house in all_houses for house in all_houses
] ]
return HousesListResponse(houses=house_responses) 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 typing import Annotated
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from ..dtos.owner_detail_response import OwnerDetailResponse from ..dtos.owner.owner_detail_response import OwnerDetailResponse
from ..dtos.owner_list_response import OwnerListResponse, OwnerResponse from ..dtos.owner.owner_list_response import OwnerListResponse, OwnerResponse
from ..repositories.owner_repository import OwnerRepository from ..repositories.owner_repository import OwnerRepository
from ..repositories.user_repository import UserRepository from ..repositories.user_repository import UserRepository
router = APIRouter() router = APIRouter()
@router.get("") @router.get("")
async def get_owners( async def get_owners(
owner_repository: Annotated[OwnerRepository, Depends()], owner_repository: Annotated[OwnerRepository, Depends()],
@ -22,7 +21,6 @@ async def get_owners(
return OwnerListResponse(owners=owners_response) return OwnerListResponse(owners=owners_response)
@router.get("/{id}") @router.get("/{id}")
async def get_owner( async def get_owner(
id: str, id: str,
@ -35,3 +33,22 @@ async def get_owner(
return OwnerDetailResponse( return OwnerDetailResponse(
id=str(owner.id), user_id=str(owner.user_id), email=user.email 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: class HousePricePredictor:
""" """
Mock ML model that predicts house prices. Mock ML model that predicts house prices.
In a real scenario, this would load a trained model. In a real scenario, this would load a trained model.
""" """
def __init__(self):
async def predict( self.model = joblib.load("backend/app/ai_models/price_predictor.pkl")
self, square_feet: float, bedrooms: int, bathrooms: float
) -> float: def predict(self, features: HouseFeatures) -> float:
base_price = square_feet * 200 X = np.array([[features.square_feet, features.bedrooms, features.bathrooms]])
bedroom_value = bedrooms * 25000 return self.model.predict(X)[0] * 100000
bathroom_value = bathrooms * 15000
predicted_price = base_price + bedroom_value + bathroom_value
return predicted_price

View File

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