Predicted progress! From awful to just "very bad"

This commit is contained in:
Артём Алейкин 2024-10-14 18:19:44 +04:00
parent 07f60e449b
commit 65dfaec1a5
11 changed files with 384 additions and 153 deletions

View File

@ -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.")

Binary file not shown.

Binary file not shown.

View File

@ -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)

View File

@ -1,9 +1,10 @@
fastapi
fastapi~=0.115.2
uvicorn
sqlalchemy
sqlalchemy~=2.0.35
psycopg2-binary
pydantic
joblib
pandas
numpy
python-dotenv
pydantic~=2.9.2
joblib~=1.4.2
pandas~=2.2.3
numpy~=2.1.2
python-dotenv
scikit-learn~=1.5.2

View File

@ -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

View File

@ -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()

View File

@ -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'.")

123
services/ml/modelBuilder.py Normal file
View File

@ -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()

View File

@ -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'.")