diff --git a/controllers/controller.py b/controllers/controller.py index b354602..219b23b 100644 --- a/controllers/controller.py +++ b/controllers/controller.py @@ -6,8 +6,8 @@ import os router = APIRouter() # Инициализация сервиса -MODEL_PATH = os.getenv("MODEL_PATH", "laptop_price_model.pkl") -FEATURE_COLUMNS_PATH = os.getenv("FEATURE_COLUMNS_PATH", "feature_columns.pkl") +MODEL_PATH = os.getenv("MODEL_PATH", "services/laptop_price_model.pkl") +FEATURE_COLUMNS_PATH = os.getenv("FEATURE_COLUMNS_PATH", "services/feature_columns.pkl") laptop_service = LaptopService(model_path=MODEL_PATH, feature_columns_path=FEATURE_COLUMNS_PATH) @router.post("/predict_price/", response_model=PredictPriceResponse, summary="Predict laptop price", description="Predict the price of a laptop based on its specifications.", response_description="The predicted price of the laptop.") diff --git a/laptops.csv b/datasets/laptops.csv similarity index 100% rename from laptops.csv rename to datasets/laptops.csv diff --git a/feature_columns.pkl b/feature_columns.pkl deleted file mode 100644 index 04a15d6..0000000 Binary files a/feature_columns.pkl and /dev/null differ diff --git a/laptop_price_model.pkl b/laptop_price_model.pkl deleted file mode 100644 index 0f62885..0000000 Binary files a/laptop_price_model.pkl and /dev/null differ diff --git a/models/models.py b/models/models.py index 1845e06..bec0914 100644 --- a/models/models.py +++ b/models/models.py @@ -1,12 +1,19 @@ -from sqlalchemy import Column, Integer, String, Float +from sqlalchemy import Column, Integer, Float, String from database import Base + class Laptop(Base): __tablename__ = "laptops" id = Column(Integer, primary_key=True, index=True) + brand = Column(String, index=True) processor = Column(String, index=True) ram = Column(Integer) os = Column(String, index=True) ssd = Column(Integer) display = Column(Float) + gpu = Column(String, index=True) + weight = Column(Float) + battery_size = Column(Integer) + release_year = Column(Integer) + display_type = Column(String, index=True) diff --git a/requirements.txt b/requirements.txt index da37b04..ded3bf3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,10 @@ -fastapi +fastapi~=0.115.2 uvicorn -sqlalchemy +sqlalchemy~=2.0.35 psycopg2-binary -pydantic -joblib -pandas -numpy -python-dotenv \ No newline at end of file +pydantic~=2.9.2 +joblib~=1.4.2 +pandas~=2.2.3 +numpy~=2.1.2 +python-dotenv +scikit-learn~=1.5.2 \ No newline at end of file diff --git a/schemas/schemas.py b/schemas/schemas.py index 39a2b85..05bd7ff 100644 --- a/schemas/schemas.py +++ b/schemas/schemas.py @@ -1,19 +1,32 @@ from pydantic import BaseModel +from typing import Optional class LaptopCreate(BaseModel): + brand: str processor: str ram: int os: str ssd: int display: float + gpu: str + weight: float + battery_size: int + release_year: int + display_type: str class LaptopResponse(BaseModel): id: int + brand: str processor: str ram: int os: str ssd: int display: float + gpu: str + weight: float + battery_size: int + release_year: int + display_type: str class Config: orm_mode = True diff --git a/services/ml/feature_importances.py b/services/ml/feature_importances.py new file mode 100644 index 0000000..accc13c --- /dev/null +++ b/services/ml/feature_importances.py @@ -0,0 +1,26 @@ +import matplotlib.pyplot as plt +import joblib +import numpy as np + +from services.ml.modelBuilder import X_train + +# Загрузка модели и признаков +model_rf = joblib.load('laptop_price_model.pkl') +feature_columns = joblib.load('feature_columns.pkl') + +# Получение важности признаков +importances = model_rf.feature_importances_ +indices = np.argsort(importances)[::-1] + +# Вывод наиболее важных признаков +print("Важность признаков:") +for f in range(X_train.shape[1]): + print(f"{f + 1}. {feature_columns[indices[f]]} ({importances[indices[f]]})") + +# Визуализация важности признаков +plt.figure(figsize=(12, 8)) +plt.title("Важность признаков (Random Forest)") +plt.bar(range(X_train.shape[1]), importances[indices], align='center') +plt.xticks(range(X_train.shape[1]), [feature_columns[i] for i in indices], rotation=90) +plt.tight_layout() +plt.show() diff --git a/services/ml/generate_synthetic_data.py b/services/ml/generate_synthetic_data.py new file mode 100644 index 0000000..f21e2c6 --- /dev/null +++ b/services/ml/generate_synthetic_data.py @@ -0,0 +1,204 @@ +import pandas as pd +import numpy as np +import random +import re +from datetime import datetime + +# Установка случайного зерна для воспроизводимости +np.random.seed(42) +random.seed(42) + +# Определение возможных значений для категориальных признаков +brands = ['Dell', 'HP', 'Lenovo', 'Apple', 'Asus', 'Acer', 'MSI', 'Microsoft', 'Samsung', 'Toshiba'] +processors = [ + 'Intel Core i3 10th Gen', 'Intel Core i5 10th Gen', 'Intel Core i7 10th Gen', + 'AMD Ryzen 3 4000 Series', 'AMD Ryzen 5 4000 Series', 'AMD Ryzen 7 4000 Series' +] +oss = ['Windows 10', 'Windows 11', 'macOS', 'Linux'] +gpus = ['Integrated', 'NVIDIA GeForce GTX 1650', 'NVIDIA GeForce RTX 3060', 'AMD Radeon RX 5600M'] +display_sizes = [13.3, 14.0, 15.6, 17.3] +display_types = ['HD', 'Full HD', '4K', 'OLED'] +ram_options = [4, 8, 16, 32] # в GB +ssd_options = [0, 256, 512, 1024] # в GB +weights = [1.2, 1.5, 2.0, 2.5, 3.0] # в кг +battery_sizes = [45, 60, 70, 90, 100] # в Вт⋅ч +release_years = list(range(2015, datetime.now().year + 1)) # от 2015 до текущего года + + +# Функции для генерации признаков +def generate_brand(): + return random.choice(brands) + + +def generate_processor(): + return random.choice(processors) + + +def generate_os(): + return random.choice(oss) + + +def generate_gpu(): + return random.choice(gpus) + + +def generate_display(): + return random.choice(display_sizes) + + +def generate_display_type(): + return random.choice(display_types) + + +def generate_ram(): + return random.choice(ram_options) + + +def generate_ssd(): + return random.choice(ssd_options) + + +def generate_weight(): + return random.choice(weights) + + +def generate_battery_size(): + return random.choice(battery_sizes) + + +def generate_release_year(): + return random.choice(release_years) + + +# Функция для расчёта цены +def calculate_price(brand, processor, ram, os, ssd, display, gpu, weight, battery_size, release_year, display_type): + base_price = 30000 # базовая цена в условных единицах + + # Добавление стоимости в зависимости от бренда + brand_premium = { + 'Apple': 40000, + 'MSI': 35000, + 'Dell': 15000, + 'HP': 12000, + 'Lenovo': 10000, + 'Microsoft': 18000, + 'Asus': 8000, + 'Acer': 7000, + 'Samsung': 9000, + 'Toshiba': 8500 + } + base_price += brand_premium.get(brand, 10000) # дефолтный премиум + + # Добавление стоимости в зависимости от процессора + if 'i3' in processor or 'Ryzen 3' in processor: + base_price += 5000 + elif 'i5' in processor or 'Ryzen 5' in processor: + base_price += 10000 + elif 'i7' in processor or 'Ryzen 7' in processor: + base_price += 15000 + + # Добавление стоимости за RAM + base_price += ram * 2000 # 2000 условных единиц за каждый GB RAM + + # Добавление стоимости за ОС + if os == 'Windows 11': + base_price += 5000 + elif os == 'macOS': + base_price += 15000 + elif os == 'Linux': + base_price += 3000 + # Windows 10 считается стандартной и не добавляет стоимости + + # Добавление стоимости за SSD + if ssd > 0: + base_price += ssd * 100 # 100 условных единиц за каждый GB SSD + + # Добавление стоимости за размер дисплея + base_price += (display - 13) * 5000 # 5000 условных единиц за каждый дюйм больше 13" + + # Добавление стоимости за тип дисплея + display_type_premium = { + 'HD': 0, + 'Full HD': 5000, + '4K': 15000, + 'OLED': 20000 + } + base_price += display_type_premium.get(display_type, 0) + + # Добавление стоимости за GPU + gpu_premium = { + 'Integrated': 0, + 'NVIDIA GeForce GTX 1650': 15000, + 'NVIDIA GeForce RTX 3060': 25000, + 'AMD Radeon RX 5600M': 20000 + } + base_price += gpu_premium.get(gpu, 0) + + # Добавление стоимости за вес (легкие ноутбуки дороже) + base_price += (3.0 - weight) * 5000 # Чем легче, тем дороже + + # Добавление стоимости за размер батареи + base_price += battery_size * 100 # 100 условных единиц за каждый Вт⋅ч батареи + + # Добавление стоимости за год выпуска (новые модели дороже) + current_year = datetime.now().year + base_price += (current_year - release_year) * 2000 # 2000 условных единиц за каждый год назад + + # Добавление случайного шума для реалистичности + noise = np.random.normal(0, 5000) # среднее 0, стандартное отклонение 5000 + final_price = base_price + noise + + return max(round(final_price, 2), 5000) # минимальная цена 5000 условных единиц + + +# Функция для генерации синтетических данных +def generate_synthetic_data(num_samples=100000): + data = [] + for _ in range(num_samples): + brand = generate_brand() + processor = generate_processor() + os = generate_os() + gpu = generate_gpu() + display = generate_display() + display_type = generate_display_type() + ram = generate_ram() + ssd = generate_ssd() + weight = generate_weight() + battery_size = generate_battery_size() + release_year = generate_release_year() + + price = calculate_price( + brand, processor, ram, os, ssd, display, gpu, weight, battery_size, release_year, display_type + ) + + data.append({ + 'brand': brand, + 'processor': processor, + 'ram': ram, + 'os': os, + 'ssd': ssd, + 'display': display, + 'gpu': gpu, + 'weight': weight, + 'battery_size': battery_size, + 'release_year': release_year, + 'display_type': display_type, + 'price': price + }) + return pd.DataFrame(data) + + +print("Генерация синтетических данных...") +synthetic_df = generate_synthetic_data(num_samples=100000) + +# Просмотр первых нескольких строк +print("\nПример данных после генерации:") +print(synthetic_df.head()) + +# Проверка распределения цен +print("\nСтатистика по ценам:") +print(synthetic_df['price'].describe()) + +# Сохранение в CSV +synthetic_df.to_csv('synthetic_laptops.csv', index=False) +print("\nСинтетические данные сохранены в 'synthetic_laptops.csv'.") diff --git a/services/ml/modelBuilder.py b/services/ml/modelBuilder.py new file mode 100644 index 0000000..e443556 --- /dev/null +++ b/services/ml/modelBuilder.py @@ -0,0 +1,123 @@ +import pandas as pd +from sklearn.model_selection import train_test_split +from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor +from sklearn.linear_model import LinearRegression +from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score +import matplotlib.pyplot as plt +import joblib +import numpy as np + +# Шаг 1: Загрузка данных +df = pd.read_csv('../../datasets/synthetic_laptops.csv') # Убедитесь, что путь к файлу правильный + +# Шаг 2: Проверка и очистка имен столбцов +print("Имена столбцов до очистки:") +print(df.columns.tolist()) + +# Приведение имен столбцов к нижнему регистру и удаление пробелов +df.columns = df.columns.str.strip().str.lower() +print("\nИмена столбцов после очистки:") +print(df.columns.tolist()) + +# Шаг 3: Проверка наличия необходимых столбцов +required_columns = [ + 'brand', 'processor', 'ram', 'os', 'ssd', 'display', + 'gpu', 'weight', 'battery_size', 'release_year', 'display_type', 'price' +] +missing_columns = [col for col in required_columns if col not in df.columns] + +if missing_columns: + print(f"\nОтсутствуют следующие столбцы: {missing_columns}") + raise Exception(f"Отсутствуют столбцы: {missing_columns}") +else: + print("\nВсе необходимые столбцы присутствуют.") + +# Шаг 4: Удаление строк с пропущенными значениями +df = df.dropna(subset=required_columns) +print(f"\nКоличество строк после удаления пропусков: {df.shape[0]}") + +# Шаг 5: Очистка и преобразование колонок + +# Функция для очистки числовых колонок, если они строковые +def clean_numeric_column(column, remove_chars=['₹', ',', ' ']): + if column.dtype == object: + for char in remove_chars: + column = column.str.replace(char, '', regex=False) + return pd.to_numeric(column, errors='coerce') + else: + return column + +# Очистка числовых колонок (исключая 'price', если уже числовая) +numerical_columns = ['ram', 'ssd', 'display', 'weight', 'battery_size', 'release_year'] +for col in numerical_columns: + df[col] = clean_numeric_column(df[col]) + +# Проверка на пропущенные значения после очистки +df = df.dropna(subset=['price'] + numerical_columns) +print(f"\nКоличество строк после очистки числовых колонок: {df.shape[0]}") + +# Шаг 6: Выбор необходимых столбцов (все уже включены) +# df уже содержит все необходимые столбцы + +print("\nПример данных после предобработки:") +print(df.head()) + +# Шаг 7: Преобразование категориальных переменных с помощью One-Hot Encoding +categorical_features = ['brand', 'processor', 'os', 'gpu', 'display_type'] +df = pd.get_dummies(df, columns=categorical_features, drop_first=True) +print("\nИмена колонок после One-Hot Encoding:") +print(df.columns.tolist()) + +# Шаг 8: Разделение данных на обучающую и тестовую выборки +X = df.drop('price', axis=1) +y = df['price'] + +X_train, X_test, y_train, y_test = train_test_split( + X, y, test_size=0.2, random_state=42 +) + +print(f"\nРазмер обучающей выборки: {X_train.shape}") +print(f"Размер тестовой выборки: {X_test.shape}") + +# Шаг 9: Обучение моделей +model_rf = RandomForestRegressor(n_estimators=100, random_state=42) +model_rf.fit(X_train, y_train) + +print("\nМодели успешно обучены.") + +# Шаг 10: Оценка моделей +models = { + 'Random Forest': model_rf, +} + +for name, mdl in models.items(): + y_pred = mdl.predict(X_test) + mae = mean_absolute_error(y_test, y_pred) + rmse = mean_squared_error(y_test, y_pred, squared=False) + r2 = r2_score(y_test, y_pred) + print(f"{name} - MAE: {mae}, RMSE: {rmse}, R²: {r2}") + +# Шаг 11: Сохранение модели и списка признаков +joblib.dump(model_rf, 'laptop_price_model.pkl') +print("\nМодель Random Forest сохранена как 'laptop_price_model.pkl'.") + +feature_columns = X.columns.tolist() +joblib.dump(feature_columns, 'feature_columns.pkl') +print("Сохранены названия признаков в 'feature_columns.pkl'.") + +# Получение важности признаков +importances = model_rf.feature_importances_ +indices = np.argsort(importances)[::-1] + +# Вывод наиболее важных признаков +print("Важность признаков:") +for f in range(X_train.shape[1]): + print(f"{f + 1}. {feature_columns[indices[f]]} ({importances[indices[f]]})") + +# Визуализация важности признаков +plt.figure(figsize=(12, 8)) +plt.title("Важность признаков (Random Forest)") +plt.bar(range(X_train.shape[1]), importances[indices], align='center') +plt.xticks(range(X_train.shape[1]), [feature_columns[i] for i in indices], rotation=90) +plt.tight_layout() +plt.show() diff --git a/services/modelBuilder.py b/services/modelBuilder.py deleted file mode 100644 index 60c56bd..0000000 --- a/services/modelBuilder.py +++ /dev/null @@ -1,143 +0,0 @@ -import pandas as pd -from sklearn.linear_model import LinearRegression -from sklearn.model_selection import train_test_split -from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor -from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score -import joblib -import re - -# Шаг 1: Загрузка данных -df = pd.read_csv('../laptops.csv') - -# Шаг 2: Проверка и очистка имен столбцов -print("Имена столбцов до очистки:") -print(df.columns.tolist()) - -# Приведение имен столбцов к нижнему регистру и удаление пробелов -df.columns = df.columns.str.strip().str.lower() -print("\nИмена столбцов после очистки:") -print(df.columns.tolist()) - -# Шаг 3: Переименование столбцов (если необходимо) -df = df.rename(columns={ - 'processor': 'processor', - 'ram': 'ram', - 'os': 'os', - 'ssd': 'ssd', - 'display': 'display', - 'price': 'price' - # Другие столбцы можно оставить без изменений или переименовать по необходимости -}) - -# Шаг 4: Проверка наличия необходимых столбцов -required_columns = ['processor', 'ram', 'os', 'ssd', 'display', 'price'] -missing_columns = [col for col in required_columns if col not in df.columns] - -if missing_columns: - print(f"\nОтсутствуют следующие столбцы: {missing_columns}") - # Здесь можно добавить дополнительную обработку или завершить выполнение - raise Exception(f"Отсутствуют столбцы: {missing_columns}") -else: - print("\nВсе необходимые столбцы присутствуют.") - -# Шаг 5: Удаление строк с пропущенными значениями -df = df.dropna(subset=required_columns) -print(f"\nКоличество строк после удаления пропусков: {df.shape[0]}") - -# Шаг 6: Очистка и преобразование колонок - -# Функция для очистки числовых колонок, содержащих символы -def clean_numeric_column(column, remove_chars=['₹', ',', ' ']): - for char in remove_chars: - column = column.str.replace(char, '', regex=False) - return pd.to_numeric(column, errors='coerce') - -# Очистка колонки 'price' -df['price'] = clean_numeric_column(df['price']) - -# Очистка колонки 'ram' (например, '16 GB DDR4 RAM' -> 16) -def extract_numeric_ram(ram_str): - match = re.search(r'(\d+)', ram_str) - if match: - return int(match.group(1)) - else: - return None - -df['ram'] = df['ram'].apply(extract_numeric_ram) - -# Очистка колонки 'ssd' (например, '512 GB SSD' -> 512) -def extract_numeric_ssd(ssd_str): - match = re.search(r'(\d+)', ssd_str) - if match: - return int(match.group(1)) - else: - return None - -df['ssd'] = df['ssd'].apply(extract_numeric_ssd) - -# Очистка колонки 'display' (убираем лишние символы, если есть) -def clean_display(display_str): - match = re.search(r'([\d.]+)', display_str) - if match: - return float(match.group(1)) - else: - return None - -df['display'] = df['display'].apply(clean_display) - -# Проверка на пропущенные значения после очистки -df = df.dropna(subset=['price', 'ram', 'ssd', 'display']) -print(f"\nКоличество строк после очистки числовых колонок: {df.shape[0]}") - -# Шаг 7: Выбор необходимых столбцов -df = df[required_columns] -print("\nПример данных после предобработки:") -print(df.head()) - -# Шаг 8: Преобразование категориальных переменных с помощью One-Hot Encoding -df = pd.get_dummies(df, columns=['processor', 'os'], drop_first=True) -print("\nИмена колонок после One-Hot Encoding:") -print(df.columns.tolist()) - -# Шаг 9: Разделение данных на обучающую и тестовую выборки -X = df.drop('price', axis=1) -y = df['price'] - -X_train, X_test, y_train, y_test = train_test_split( - X, y, test_size=0.2, random_state=42 -) - -print(f"\nРазмер обучающей выборки: {X_train.shape}") -print(f"Размер тестовой выборки: {X_test.shape}") - -# Шаг 10: Обучение модели -model = RandomForestRegressor(n_estimators=100, random_state=42) -model.fit(X_train, y_train) - -# Обучение моделей -gbr = GradientBoostingRegressor(n_estimators=100, random_state=42) -gbr.fit(X_train, y_train) - -lr = LinearRegression() -lr.fit(X_train, y_train) -print("\nМодели успешно обучена.") - -# Оценка моделей - -models = {'Random Forest': model, 'Gradient Boosting': gbr, 'Linear Regression': lr} - -for name, mdl in models.items(): - y_pred = mdl.predict(X_test) - mae = mean_absolute_error(y_test, y_pred) - rmse = mean_squared_error(y_test, y_pred, squared=False) - r2 = r2_score(y_test, y_pred) - print(f"{name} - MAE: {mae}, RMSE: {rmse}, R²: {r2}") - -# Шаг 12: Сохранение модели -joblib.dump(model, '../laptop_price_model.pkl') -print("\nМодель сохранена как 'laptop_price_model.pkl'.") - -# Дополнительно: Сохранение колонок, полученных после One-Hot Encoding, для использования в бэкенде -feature_columns = X.columns.tolist() -joblib.dump(feature_columns, '../feature_columns.pkl') -print("Сохранены названия признаков в 'feature_columns.pkl'.")