feat: house price prediction model integration
This commit is contained in:
parent
72375db5cd
commit
3aa78be3d6
33
backend/app/ai_models/california.py
Normal file
33
backend/app/ai_models/california.py
Normal 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')
|
||||
BIN
backend/app/ai_models/price_predictor.pkl
Normal file
BIN
backend/app/ai_models/price_predictor.pkl
Normal file
Binary file not shown.
34
backend/app/dtos/house/house_create_request.py
Normal file
34
backend/app/dtos/house/house_create_request.py
Normal 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]
|
||||
)
|
||||
5
backend/app/dtos/house/house_create_response.py
Normal file
5
backend/app/dtos/house/house_create_response.py
Normal file
@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HouseCreateResponse(BaseModel):
|
||||
id: str
|
||||
8
backend/app/dtos/house/house_features.py
Normal file
8
backend/app/dtos/house/house_features.py
Normal 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")
|
||||
7
backend/app/dtos/house/house_predict_request.py
Normal file
7
backend/app/dtos/house/house_predict_request.py
Normal file
@ -0,0 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HousePricePredictionRequest(BaseModel):
|
||||
square_feet: float
|
||||
bedrooms: int
|
||||
bathrooms: float
|
||||
5
backend/app/dtos/house/house_predict_response.py
Normal file
5
backend/app/dtos/house/house_predict_response.py
Normal file
@ -0,0 +1,5 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HousePricePredictionResponse(BaseModel):
|
||||
predicted_price: float
|
||||
10
backend/app/dtos/house/house_response.py
Normal file
10
backend/app/dtos/house/house_response.py
Normal file
@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class HouseResponse(BaseModel):
|
||||
id: str
|
||||
description: str
|
||||
address: str
|
||||
city: str
|
||||
country: str
|
||||
price: float
|
||||
6
backend/app/dtos/house/houses_list_response.py
Normal file
6
backend/app/dtos/house/houses_list_response.py
Normal file
@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.app.dtos.house.house_response import HouseResponse
|
||||
|
||||
class HousesListResponse(BaseModel):
|
||||
houses: list[HouseResponse]
|
||||
7
backend/app/dtos/owner/owner_detail_response.py
Normal file
7
backend/app/dtos/owner/owner_detail_response.py
Normal file
@ -0,0 +1,7 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OwnerDetailResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
email: str
|
||||
10
backend/app/dtos/owner/owner_list_response.py
Normal file
10
backend/app/dtos/owner/owner_list_response.py
Normal file
@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class OwnerResponse(BaseModel):
|
||||
id: str
|
||||
user_id: str
|
||||
|
||||
|
||||
class OwnerListResponse(BaseModel):
|
||||
owners: list[OwnerResponse]
|
||||
6
backend/app/dtos/user/user_list_response.py
Normal file
6
backend/app/dtos/user/user_list_response.py
Normal file
@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.app.dtos.user.user_response import UserResponse
|
||||
|
||||
class UserListResponse(BaseModel):
|
||||
users: list[UserResponse]
|
||||
6
backend/app/dtos/user/user_response.py
Normal file
6
backend/app/dtos/user/user_response.py
Normal file
@ -0,0 +1,6 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
id: str
|
||||
email: str
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
@ -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")
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
)
|
||||
|
||||
@ -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
|
||||
@ -8,3 +8,5 @@ python-dotenv
|
||||
pg8000
|
||||
asyncpg
|
||||
greenlet
|
||||
botbuilder-core
|
||||
openai
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user