[AICE PROFESSIONAL 준비] 랩탑 가격 예측

킹용맨 ㅣ 2026. 6. 17. 18:27

캐글의 [Laptop Specifications and Price Prediction Dataset] 을 이용하여 랩탑 가격을 예측하는 선형회귀 모델을 만들었다.

 

https://www.kaggle.com/datasets/muhammadmusharraf444/laptop-specifications-and-price-prediction-dataset

 

Laptop Specifications and Price Prediction Dataset

Dataset About Comprehensive Laptop Specifications and Price Prediction Dataset

www.kaggle.com

 

데이터 로드 및 확인

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings

warnings.filterwarnings('ignore', category=UserWarning)

 

df = pd.read_csv("./data/laptop_data.csv")
df

 

df.info()

 

df.describe()

 

df.isnull().sum()

 

 

데이터 시각화

numerical_cols = ["Unnamed: 0", "Inches", "Price"]
categorical_cols = [
    "Company",
    "TypeName",
    "ScreenResolution",
    "Cpu",
    "Ram",
    "Memory",
    "Gpu",
    "OpSys",
    "Weight"
    ]

# 수치형 데이터 시각화
fig, axes = plt.subplots(1, 3, figsize=(15, 5))

for i, col_name in enumerate(numerical_cols):
    ax = axes[i]

    # 히스토그램 그리기
    ax.hist(df[col_name], bins=10, color="C"+str(i), alpha=0.7, edgecolor="black")

    # 개별 차트 제목 및 라벨 설정
    ax.set_title(f"Histogram: {col_name}")
    ax.set_xlabel("Values")
    ax.set_ylabel("Frequency")

    # 격자선 추가
    ax.grid(axis="y", alpha=0.5)

plt.tight_layout()
plt.show()

 

# 범주형 데이터 시각화
fig, axes = plt.subplots(3, 3, figsize=(20, 20))
axes_flat = axes.flatten()

for i, col_name in enumerate(categorical_cols):
    ax = axes_flat[i]

    # 카테고리 수가 많아서 상위 15개만 뽑고 차트를 그림
    data_counts = df[col_name].value_counts().head(15)

    ax.bar(data_counts.index.astype(str), data_counts.values, color="C"+str(i), alpha=0.7, edgecolor="black")
    ax.set_title(f"Bar Chart: {col_name}")
    ax.set_ylabel("Count")
    ax.tick_params(axis="x", rotation=90) # x축 글자가 겹치므로 90도 회전

plt.tight_layout()
plt.show()

 

for col in categorical_cols:
    print(f"===== {col} : {df[col].nunique()} =====")
    catetories = df[col].unique().tolist()
    print(catetories)

 

데이터값의 종류가 상당히 많은 컬럼들이 존재한다. Cpu, Gpu 등은 브랜드, 속도, 모델명등이 다양하기 때문에 100종류가 넘는 값을 가지고 있는데, 이는 추후 특징을 추출하여 단순화해야 할 필요가 있다.

 

plt.figure(figsize=(8,5))

sns.boxplot(data=df, x='Company', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

plt.figure(figsize=(8,5))

sns.boxplot(data=df, x='TypeName', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

plt.figure(figsize=(8,5))

sns.boxplot(data=df, x='Ram', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

plt.figure(figsize=(8,5))

sns.boxplot(data=df, x='Memory', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

plt.figure(figsize=(8,5))

sns.boxplot(data=df, x='OpSys', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

plt.figure(figsize=(15,10))

sns.boxplot(data=df, x='ScreenResolution', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

plt.figure(figsize=(15,10))

sns.boxplot(data=df, x='Cpu', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

plt.figure(figsize=(15,10))

sns.boxplot(data=df, x='Gpu', y='Price')
plt.tick_params(axis="x", rotation=90)
plt.tight_layout()
plt.show()

 

# Weight 값이 모두 숫자로 시작해서 kg 문자열로 끝나는지 확인
# ^\d+ : 시작이 숫자로 이루어짐
# (\.\d+)? : 선택적으로 소수점이 올 수 있음
# Kg$ : 끝이 'Kg'로 끝남
pattern = r"^\d+(\.\d+)?kg$"
df[~df["Weight"].str.contains(pattern, na=False)]

invalid_data = df[~df['Weight'].str.contains(pattern, na=False)]
print(invalid_data)

 

# Ram 값이 모두 숫자로 시작해서 GB 문자열로 끝나는지 확인
# ^\d+ : 시작이 숫자로 이루어짐
# GB$ : 끝이 'GB'로 끝남
pattern = r"^\d+GB$"
df[~df["Ram"].str.contains(pattern, na=False)]

invalid_data = df[~df['Ram'].str.contains(pattern, na=False)]
print(invalid_data)

 

 

데이터 특징 정리 및 전처리

1. 가격대는 왼쪽(저렴한 가격)으로 쏠려 있다.
2. 게이밍, 워크스테이션 노트북이 주로 높은 가격대를 형성하고 있다.
3. 램 용량은 가격와 양의 상관관계를 보인다.
4. 4K 해상도 노트북이 주로 높은 가격대를 형성하고 있다.
5. CPU, GPU는 종류가 매우 다양하기 때문에 데이터 정제 및 압축이 필요하다.
6. [Unnamed: 0] 컬럼은 삭제
7. [Weight] 컬럼은 숫자로 시작하고 "Kg"으로 끝나므로, "Kg" 문자열은 삭제
8. [Ram] 컬럼은 숫자로 시작하고 "GB"로 끝나므로 "GB" 문자열은 삭제

 

df_drop = df.drop(labels=["Unnamed: 0"], axis=1)
df_drop['Weight'] = df_drop['Weight'].str.replace("kg", "").astype(float)
df_drop['Ram'] = df_drop['Ram'].str.replace("GB", "").astype(int)
df_drop.head()

 

from sklearn.preprocessing import OneHotEncoder

# Company, TypeName, OpSys 는 원핫인코딩
encoder = OneHotEncoder(sparse_output=False)
encoded_data = encoder.fit_transform(df_drop[["Company", "TypeName", "OpSys"]])

# 인코딩 된 데이터를 데이터프레임으로 변환
encoded_df = pd.DataFrame(
    encoded_data, columns=encoder.get_feature_names_out(["Company", "TypeName", "OpSys"])
)

# 기존 데이터셋에서 인코딩한 컬럼을 제거하고 인코딩된 데이터를 붙이기
df_encoded = df_drop.drop(columns=["Company", "TypeName", "OpSys"])
df_encoded = pd.concat([df_encoded, encoded_df], axis=1)

 

# ScreenResolution은 IPS패널, 터치스크린, 레티나, [X해상도xY해상도] 세 가지 대표값으로 분리 (추후 삭제))
# 1. 해상도 분리
df_encoded["X_res"] = df_encoded["ScreenResolution"].str.extract(r"(\d+)x\d+").astype(int)
df_encoded["Y_res"] = df_encoded["ScreenResolution"].str.extract(r"\d+x(\d+)").astype(int)

# 2. IPS 패널 여부
df_encoded["IPS"] = df_encoded["ScreenResolution"].str.contains("IPS").astype(int)

# 3. 터치스크린 여부
df_encoded["TouchScreen"] = df_encoded["ScreenResolution"].str.contains("TouchScreen").astype(int)

# 4. 레티나 디스플레이 여부
df_encoded["Retina"] = df_encoded["ScreenResolution"].str.contains("Retina").astype(int)

 

import re

# 메모리 용량을 처리하기 위한 로직: 모든 값을 GB 단위로 변환
def convert_to_gb(value):
    value = value.upper()
    if "TB" in value:
        num = float(re.findall(r"(\d+\.?\d*)", value)[0])
        return num * 1000
    elif "GB" in value:
        num = float(re.findall(r"(\d+\.?\d*)", value)[0])
        return num
    return 0

# 타입별로 용량 합산 (문자열을 + 기준으로 나눠서 각 저장장치의 용량과 타입을 파악)
def parse_storage(row):
    ssd, hdd, flash, hybrid = 0, 0, 0, 0
    parts = row.split("+")
    for part in parts:
        part = part.strip()
        gb = convert_to_gb(part)

        if "SSD" in part:
            ssd += gb
        elif "HDD" in part:
            hdd += gb
        elif "Flash" in part:
            flash += gb
        elif "Hybrid" in part:
            hybrid += gb
    return pd.Series([ssd, hdd, flash, hybrid, ssd + hdd + flash + hybrid])
    
    # 데이터 적용
df_encoded[["SSD_GB", "HDD_GB", "Flash_GB", "Hybrid_GB", "Total_GB"]] = df_encoded["Memory"].apply(parse_storage)

 

# CPU 데이터 압축 전략
# 브랜드, 라인업, 클럭 속도 추출하여 분리 저장
def parse_cpu(cpu_string):
    # 1. 클럭 속도 추출 (마지막에 오는 숫자 + GHz)
    speed = float(re.findall(r"(\.?\d*)GHz", cpu_string)[0])

    # 2. 브랜드 추철 (문자열 첫 단어)
    brand = cpu_string.split()[0]

    # 3. 모델 라인업 분류
    if "Core i7" in cpu_string:
        lineup = "Core i7"
    elif "Core i5" in cpu_string:
        lineup = "Core i5"
    elif "Core i3" in cpu_string:
        lineup = "Core i3"
    elif "Celeron" in cpu_string:
        lineup = "Celeron"
    elif "Pentium" in cpu_string:
        lineup = "Pentium"
    elif "Atom" in cpu_string:
        lineup = "Atom"
    elif "Core M" in cpu_string:
        lineup = "Core M"
    elif "Xeon" in cpu_string:
        lineup = "Xeon"
    elif "A9-Series" in cpu_string:
        lineup = "A9-Series"
    elif "A6-Series" in cpu_string:
        lineup = "A6-Series"
    elif "A8-Series" in cpu_string:
        lineup = "A8-Series"
    elif "A12-Series" in cpu_string:
        lineup = "A12-Series"
    elif "A10-Series" in cpu_string:
        lineup = "A10-Series"
    elif "FX" in cpu_string:
        lineup = "FX"
    elif "E-Series" in cpu_string:
        lineup = "E2-Series"
    elif "Ryzen" in cpu_string:
        lineup = "Ryzen"
    else:
        lineup = "Other"

    return pd.Series([brand, lineup, speed])

# 데이터 적용
df_encoded[["CPU_Brand", "CPU_Lineup", "CPU_Speed"]] = df_encoded["Cpu"].apply(parse_cpu)

 

# Gpu 데이터 압축 전략
# 브랜드, 성능 등급화한 값을 추출하여 분리 저장
# 브랜드: Intel, Nvidia, AMD, ARM
def categorize_gpu(gpu_string):
    gpu_string = gpu_string.upper()

    # 1. 제조사 추출
    if "NVIDIA" in gpu_string:
        brand = "NVIDIA"
    elif "AMD" in gpu_string:
        brand = "AMD"
    elif "INTEL" in gpu_string:
        brand = "INTEL"
    else:
        brand = "Other"

    # 성능 등급 추출 및 단순화
    if "GTX" in gpu_string:
        model = "GTX"
    elif "UHD" in gpu_string:
        model = "UHD"
    elif "HD" in gpu_string:
        model = "HD"
    elif "MX" in gpu_string:
        model = "MX"
    elif "QUADRO" in gpu_string:
        model = "QUADRO"
    elif "RADEON" in gpu_string:
        model = "RADEON"
    elif "IRIS" in gpu_string:
        model = "IRIS"
    else:
        model = "Other"

    return pd.Series([brand, model])

# 데이터 적용
df_encoded[["GPU_Brand", "GPU_ModelType"]] = df_encoded["Gpu"].apply(categorize_gpu)

 

# 전처리를 끝낸 컬럼 삭제
df_encoded = df_encoded.drop(labels=["ScreenResolution", "Cpu", "Memory", "Gpu", "Weight"], axis=1)

# 전처리 후 문자열 형태로 남아 있는 컬럼은 모두 원핫 인코딩
encoder2 = OneHotEncoder(sparse_output=False)
encoded_data2 = encoder2.fit_transform(df_encoded[["CPU_Brand", "CPU_Lineup", "GPU_Brand", "GPU_ModelType"]])

# 인코딩 된 데이터를 데이터프레임으로 변환
encoded_df2 = pd.DataFrame(
    encoded_data2, columns=encoder2.get_feature_names_out(["CPU_Brand", "CPU_Lineup", "GPU_Brand", "GPU_ModelType"])
)

# 기존 데이터셋에서 인코딩한 컬럼을 제거하고 인코딩된 데이터를 붙이기
df_encoded = df_encoded.drop(columns=["CPU_Brand", "CPU_Lineup", "GPU_Brand", "GPU_ModelType"])
df_encoded = pd.concat([df_encoded, encoded_df2], axis=1)
df_encoded

 

 

학습데이터, 테스트데이터 분리 및 모델 학습

from sklearn.model_selection import train_test_split

X = df_encoded.drop(labels=["Price"], axis=1)
y = df_encoded["Price"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

 

from sklearn.tree import DecisionTreeRegressor
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from xgboost import XGBRegressor

lr = LinearRegression()
dtr = DecisionTreeRegressor()
rfr = RandomForestRegressor()
xgbr = XGBRegressor()

 

from sklearn.metrics import mean_squared_error, r2_score

def model_eval(model):
    print(f"===== Model {model} Evaluation =====")
    print(f"Train: {model.score(X_train, y_train)}")
    print(f"Test: {model.score(X_test, y_test)}")

    predictions = model.predict(X_test)
    rmse = np.sqrt(mean_squared_error(y_test, predictions))
    r2 = r2_score(y_test, predictions)

    print(f"RMSE: {rmse}")
    print(f"R2 Score: {r2}")
    print("=" * 50)

 

lr.fit(X_train, y_train)
dtr.fit(X_train, y_train)
rfr.fit(X_train, y_train)
xgbr.fit(X_train, y_train)

 

 

모델 평가

models = [lr, dtr, rfr, xgbr]

for model in models:
    model_eval(model)

 

 

모델 저장

import joblib

joblib.dump(rfr, "./my_rfr_model.pkl")

 

 

가상으로 만든 데이터를 이용하여 예측하기

# price 값이 없는 가상의 데이터를 만들어서 예측
df = pd.read_csv("./data/real_data.csv")
df.head()

 

# 기존과 동일한 전처리 과정을 진행
df_drop = df.drop(labels=["Unnamed: 0"], axis=1)
df_drop['Weight'] = df_drop['Weight'].str.replace("kg", "").astype(float)
df_drop['Ram'] = df_drop['Ram'].str.replace("GB", "").astype(int)
df_drop.head()

encoded_data = encoder.transform(df_drop[["Company", "TypeName", "OpSys"]])

encoded_df = pd.DataFrame(
    encoded_data, columns=encoder.get_feature_names_out(["Company", "TypeName", "OpSys"])
)

df_encoded = df_drop.drop(columns=["Company", "TypeName", "OpSys"])
df_encoded = pd.concat([df_encoded, encoded_df], axis=1)
df_encoded

df_encoded["X_res"] = df_encoded["ScreenResolution"].str.extract(r"(\d+)x\d+").astype(int)
df_encoded["Y_res"] = df_encoded["ScreenResolution"].str.extract(r"\d+x(\d+)").astype(int)

df_encoded["IPS"] = df_encoded["ScreenResolution"].str.contains("IPS").astype(int)

df_encoded["TouchScreen"] = df_encoded["ScreenResolution"].str.contains("TouchScreen").astype(int)

df_encoded["Retina"] = df_encoded["ScreenResolution"].str.contains("Retina").astype(int)

df_encoded[["SSD_GB", "HDD_GB", "Flash_GB", "Hybrid_GB", "Total_GB"]] = df_encoded["Memory"].apply(parse_storage)

df_encoded[["CPU_Brand", "CPU_Lineup", "CPU_Speed"]] = df_encoded["Cpu"].apply(parse_cpu)

df_encoded[["GPU_Brand", "GPU_ModelType"]] = df_encoded["Gpu"].apply(categorize_gpu)

df_encoded = df_encoded.drop(labels=["ScreenResolution", "Cpu", "Memory", "Gpu", "Weight"], axis=1)

encoded_data2 = encoder2.transform(df_encoded[["CPU_Brand", "CPU_Lineup", "GPU_Brand", "GPU_ModelType"]])

encoded_df2 = pd.DataFrame(
    encoded_data2, columns=encoder2.get_feature_names_out(["CPU_Brand", "CPU_Lineup", "GPU_Brand", "GPU_ModelType"])
)

df_encoded = df_encoded.drop(columns=["CPU_Brand", "CPU_Lineup", "GPU_Brand", "GPU_ModelType"])
df_encoded = pd.concat([df_encoded, encoded_df2], axis=1)
df_encoded

 

predictions = rfr.predict(df_encoded)
predictions

 

df["Price"] = predictions
df.to_csv("./result.csv")
print("파일이 저장되었습니다.")