선형회귀분석 - 중고차 가격에 영향을 미치는 요인 분석
1. 데이터셋 불러오기 및 확인
kaggle에서 제공하는 중고차 데이터셋을 불러옵니다.
여러 변수들의 전처리 과정을 거치고, 선형회귀분석을 통해 중고차 가격에 영향을 미치는 요인을 분석합니다.
데이터출처링크: Kaggle - Used Car Dataset
Used Car Dataset
| Column | Description | 
|---|---|
| car_name | 모델명 | 
| registration_year | 등록 날짜 | 
| insurance_validity | 보험 유효성 | 
| fuel_type | 연료 타입 | 
| seats | 좌석 수 | 
| kms_driven | 총 주행거리 | 
| ownership | 소유권 형식 | 
| transmission | 변속장치 타입 | 
| manufacturing_year | 제작 년도 | 
| mileage(kmpl) | 연비 (Kilometers per Liter) | 
| engine(cc) | 엔진 (cc) | 
| max_power(bhp) | 제동 마력 (Brake horse power = 실제 마력) | 
| torque(Nm) | 토크 (Newton meter) => 변수 삭제 | 
| price(in lakhs) | 가격 (1 lakh = 100,000 루피) | 
import pandas as pd
origin = pd.read_csv("/Used Car Dataset.csv", index_col=0)
1-1 데이터 정보 확인
origin.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1553 entries, 0 to 1552
Data columns (total 14 columns):
 #   Column              Non-Null Count  Dtype
---  ------              --------------  -----
 0   car_name            1553 non-null   object
 1   registration_year   1553 non-null   object
 2   insurance_validity  1553 non-null   object
 3   fuel_type           1553 non-null   object
 4   seats               1553 non-null   int64
 5   kms_driven          1553 non-null   int64
 6   ownsership          1553 non-null   object
 7   transmission        1553 non-null   object
 8   manufacturing_year  1553 non-null   object
 9   mileage(kmpl)       1550 non-null   float64
 10  engine(cc)          1550 non-null   float64
 11  max_power(bhp)      1550 non-null   float64
 12  torque(Nm)          1549 non-null   float64
 13  price(in lakhs)     1553 non-null   float64
dtypes: float64(5), int64(2), object(7)
memory usage: 182.0+ KB
1-2 기술통계량 확인
- 기술통계량 확인 결과 정확하게 모든 값이 같은 변수가 있어서 제거 합니다.
 - registration_year는 manufacturing_year와의 다중공선성을 고려하여 제거합니다.
 - 제거할 변수: 
torque(Nm),registration_year 
origin.describe(include="all").T
| count | unique | top | freq | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| car_name | 1553 | 925 | 2017 BMW X1 sDrive20d Expedition | 25 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 
| registration_year | 1553 | 178 | 2017 | 40 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 
| insurance_validity | 1553 | 6 | Comprehensive | 1084 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 
| fuel_type | 1553 | 4 | Petrol | 1013 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 
| seats | 1553.0 | NaN | NaN | NaN | 91.480361 | 2403.42406 | 4.0 | 5.0 | 5.0 | 5.0 | 67000.0 | 
| kms_driven | 1553.0 | NaN | NaN | NaN | 52841.931101 | 40067.800347 | 620.0 | 30000.0 | 49134.0 | 70000.0 | 810000.0 | 
| ownsership | 1553 | 22 | First Owner | 1240 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 
| transmission | 1553 | 13 | Manual | 835 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 
| manufacturing_year | 1553 | 19 | 2018 | 236 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 
| mileage(kmpl) | 1550.0 | NaN | NaN | NaN | 236.927277 | 585.964295 | 7.81 | 16.3425 | 18.9 | 22.0 | 3996.0 | 
| engine(cc) | 1550.0 | NaN | NaN | NaN | 14718574440.514194 | 218562947745.450348 | 5.0 | 1197.0 | 1462.0 | 1995.0 | 3258640000000.0 | 
| max_power(bhp) | 1550.0 | NaN | NaN | NaN | 14718574440.514194 | 218562947745.450348 | 5.0 | 1197.0 | 1462.0 | 1995.0 | 3258640000000.0 | 
| torque(Nm) | 1549.0 | NaN | NaN | NaN | 14239.891543 | 96662.407522 | 5.0 | 400.0 | 1173.0 | 8850.0 | 1464800.0 | 
| price(in lakhs) | 1553.0 | NaN | NaN | NaN | 166.141494 | 3478.85509 | 1.0 | 4.66 | 7.14 | 17.0 | 95000.0 | 
origin.drop(["max_power(bhp)", "registration_year"], axis=1, inplace=True)
origin.columns
Index(['car_name', 'insurance_validity', 'fuel_type', 'seats', 'kms_driven',
       'ownsership', 'transmission', 'manufacturing_year', 'mileage(kmpl)',
       'engine(cc)', 'torque(Nm)', 'price(in lakhs)'],
      dtype='object')
1-3 데이터 출력 상위 5개
origin.head(5)
| car_name | insurance_validity | fuel_type | seats | kms_driven | ownsership | transmission | manufacturing_year | mileage(kmpl) | engine(cc) | torque(Nm) | price(in lakhs) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2017 Mercedes-Benz S-Class S400 | Comprehensive | Petrol | 5 | 56000 | First Owner | Automatic | 2017 | 7.81 | 2996.0 | 333.0 | 63.75 | 
| 1 | 2020 Nissan Magnite Turbo CVT XV Premium Opt BSVI | Comprehensive | Petrol | 5 | 30615 | First Owner | Automatic | 2020 | 17.40 | 999.0 | 9863.0 | 8.99 | 
| 2 | 2018 BMW X1 sDrive 20d xLine | Comprehensive | Diesel | 5 | 24000 | First Owner | Automatic | 2018 | 20.68 | 1995.0 | 188.0 | 23.75 | 
| 3 | 2019 Kia Seltos GTX Plus | Comprehensive | Petrol | 5 | 18378 | First Owner | Manual | 2019 | 16.50 | 1353.0 | 13808.0 | 13.56 | 
| 4 | 2019 Skoda Superb LK 1.8 TSI AT | Comprehensive | Petrol | 5 | 44900 | First Owner | Automatic | 2019 | 14.67 | 1798.0 | 17746.0 | 24.00 | 
1-4 결측치 확인
- 결측치의 갯수가 많지 않고 데이터 분석에 큰 영향을 미치지 않을 것으로 판단하여 제거 했습니다.
 
print(origin.isnull().sum())
origin.dropna(inplace=True)
car_name              0
insurance_validity    0
fuel_type             0
seats                 0
kms_driven            0
ownsership            0
transmission          0
manufacturing_year    0
mileage(kmpl)         3
engine(cc)            3
torque(Nm)            4
price(in lakhs)       0
dtype: int64
1-5 중복값 확인
- 다수의 중복된 중복된 값이 존재하였으므로 제거 합니다.
 
print(origin.duplicated().sum())
origin = origin.drop_duplicates(subset=origin.columns, ignore_index=True, keep="first")
420
1-6 변수명 변경
- torque 변수의 값들은 실제 bhp 변수의 값이므로 변수명 변경
 
origin.rename(
    columns={
        "ownsership": "ownership",
        "mileage(kmpl)": "mileage",
        "engine(cc)": "engine",
        "torque(Nm)": "bhp",
        "price(in lakhs)": "price",
    },
    inplace=True,
)
1-7 카테고리, 수치형 데이터 및 타겟 데이터 분류
- 
    
manufacturing_year
- 시간에 따라 의존 변수가 선형적으로 변화하고 자동차의 가치가 매년 일정 비율로 감소한다고 가정하여, 년도를 수치형 변수로 사용하였습니다.
 
 - 
    
seats
- 좌석 수의 증가가 특정 변수(차량 가격)에 미치는지를 분석하기 위해 수치형 변수로 사용하였습니다.
 
 
categories = [
    "insurance_validity",
    "fuel_type",
    "ownership",
    "transmission",
]
num_features = [
    "kms_driven",
    "mileage",
    "engine",
    "bhp",
    "manufacturing_year",
    "seats",
]
target = "price"
2. 탐색적 데이터 분석 및 데이터 전처리
2-1 ownership 변수의 값 정리
- ownership 변수에 맞지 않는 여러 값들이 발견되어 정리 합니다.
 
origin = origin.loc[
    origin["ownership"].isin(["First Owner", "Second Owner", "Third Owner"])
]
2-2 명목형 변수와 수치형 변수 타입 지정
for category in categories:
    origin[category] = origin[category].astype("category")
origin["manufacturing_year"] = origin["manufacturing_year"].astype(int)
2-3 df변수에 1차 전처리된 origin 데이터 카피
df = origin.copy()
df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1103 entries, 0 to 1128
Data columns (total 12 columns):
 #   Column              Non-Null Count  Dtype
---  ------              --------------  -----
 0   car_name            1103 non-null   object
 1   insurance_validity  1103 non-null   category
 2   fuel_type           1103 non-null   category
 3   seats               1103 non-null   int64
 4   kms_driven          1103 non-null   int64
 5   ownership           1103 non-null   category
 6   transmission        1103 non-null   category
 7   manufacturing_year  1103 non-null   int64
 8   mileage             1103 non-null   float64
 9   engine              1103 non-null   float64
 10  bhp                 1103 non-null   float64
 11  price               1103 non-null   float64
dtypes: category(4), float64(4), int64(3), object(1)
memory usage: 82.4+ KB
2-4 수치형 변수 boxplot 그래프로 이상치 확인
- engine, max_power, price 등에서 다수의 이상치가 관측 되었습니다.
 
import matplotlib.pyplot as plt
import seaborn as sns
features = ["kms_driven", "mileage", "engine", "bhp", "price"]
fig, axes = plt.subplots(3, 2, figsize=(13, 13), dpi=100)
for feature, ax in zip(features, axes.flatten()):
    sns.boxplot(data=df, y=feature, ax=ax)
    ax.grid(True)
fig.delaxes(axes[2, 1])
plt.tight_layout()
plt.show()
plt.close()

2-5. mileage(연비) 변수에 대한 이상치 확인 및 제거
- mileage값이 28.4에서 245.59로 급격하게 증가하는 이상치가 관측 되어 제거 합니다.
 
result = df.loc[df["mileage"] > 30, features].sort_values("mileage", ascending=False)
print(f"검색된 값: {result.shape[0]}")
df.drop(result.index, inplace=True)
검색된 값: 134
2-6. bhp(제동마력) 변수에 대한 이상치 확인 및 제거
- 
    
bhp의 단위가 이상할 정도로 큰 값들이 존재하여, 실제 차량 데이터와 비교해본 결과 실제 값으로 확인 되었고 단지 단위가 잘못 표기된 것으로 판단되어 전처리 과정을 거쳐 사용가능한 값으로 변환 하였습니다. (예 : 1186600 = 118.66)
 - 
    
2018 Mercedes-Benz S-Class Maybach S500 이상의 값들을 순차적으로 전처리 하였습니다.
 
thresholds = [460, 460, 600, 600]
for threshold in thresholds:
    df["bhp"] = df["bhp"].apply(lambda x: x / 10 if x > threshold else x)
    result = df.loc[df["bhp"] > threshold, features].sort_values("bhp", ascending=False)
    print(f"검색된 값: {result.shape[0]}")
검색된 값: 455
검색된 값: 11
검색된 값: 9
검색된 값: 0
2-7. price(가격) 변수에 대한 이상치 확인 및 제거
- 가격이 95000으로 기록된 이상치는 제거 합니다.
 
result = df.loc[df["price"] > 100, features].sort_values("price", ascending=False)
print(f"검색된 값: {result.shape[0]}")
result = df.loc[df["price"] > 100]
df.drop(result.index, inplace=True)
검색된 값: 1
2-7-1. 파생변수 생성하여 brand별 price 분포 확인
df["brand"] = df["car_name"].apply(lambda x: x.split(" ")[1])
df["brand"].unique()
array(['Mercedes-Benz', 'Nissan', 'BMW', 'Kia', 'Skoda', 'Honda',
       'Hyundai', 'Tata', 'Renault', 'Ford', 'Jeep', 'MG', 'Maruti',
       'Audi', 'Toyota', 'Jaguar', 'Volkswagen', 'Mahindra', 'Land',
       'Volvo', 'Isuzu', 'Mitsubishi', 'Porsche', 'Datsun', 'Lexus',
       'Mini', 'Fiat'], dtype=object)
plt.figure(figsize=(25, 10), dpi=200)
sns.boxplot(data=df, x="brand", y="price")
plt.grid()
plt.tight_layout()

상위 5개 브랜드에서 가격이 비정상적으로 낮은(5 lakhs 이하) 데이터 제거 합니다.
result = df.loc[
    (
        (df["car_name"].str.contains("benz", case=False))
        | (df["car_name"].str.contains("bmw", case=False))
        | (df["car_name"].str.contains("audi", case=False))
        | (df["car_name"].str.contains("Land", case=False))
        | (df["car_name"].str.contains("Jaguar", case=False))
    )
    & (df["price"] < 5),
    ["car_name"] + num_features + [target],
].sort_values("price", ascending=False)
df.drop(result.index, inplace=True)
2-8. 자동차 브랜드별 평균 가격을 파생변수로 생성
df["car_name_mean_price"] = df.groupby("brand")["price"].transform("mean")
2-9. kms_driven(주행거리) 변수에 대한 이상치 확인
- 79만km와 81만km는 실제로 관측될 수 있는 수치이긴 하지만, 다른 차량 대비 지나치게 높은 값으로 결과의 신뢰성을 고려하여 이상치로 판단하고 제거 합니다.
 
df.drop(df[df["kms_driven"] > 700000].index, inplace=True)
2-10. engine(cc) 변수에 대한 이상치 확인 및 제거
df.loc[df["car_name"] == "2014 Mercedes-Benz S-Class S 500 L", "engine"] = 4663
2-11. 수치형 변수 boxplot으로 이상치 재확인
features = ["kms_driven", "mileage", "engine", "bhp", "price"]
fig, axes = plt.subplots(3, 2, figsize=(13, 13), dpi=100)
for feature, ax in zip(features, axes.flatten()):
    sns.boxplot(data=df, y=feature, ax=ax)
    ax.grid(True)
fig.delaxes(axes[2, 1])
plt.tight_layout()
plt.show()
plt.close()

3. 선형회귀분석
num_features.pop(num_features.index("manufacturing_year"))
'manufacturing_year'
3-1. 히트맵 상관관계 확인
plt.figure(figsize=(10, 8), dpi=100)
sns.heatmap(
    df[num_features].join(df[target]).corr(),
    annot=True,
    cmap="coolwarm",
    fmt=".2f",
    annot_kws={"fontsize": 15},
)
plt.tight_layout()
plt.show()
plt.close()

engine과 bhp에서 price와의 강한 양의 상관관계가 관측되었고, mileage와 price의 음의 상관관계가 관측되었습니다.
분석에 사용하지 않을 변수 제거
df.drop(["car_name", "brand"], axis=1, inplace=True)
카테고리 변수 확인
for i in categories:
    print(i, df[i].unique(), end="\n\n")
    print("=" * 100, end="\n\n")
insurance_validity ['Comprehensive', 'Third Party insurance', 'Zero Dep', 'Third Party']
Categories (4, object): ['Comprehensive', 'Third Party', 'Third Party insurance', 'Zero Dep']
====================================================================================================
fuel_type ['Petrol', 'Diesel', 'CNG']
Categories (3, object): ['CNG', 'Diesel', 'Petrol']
====================================================================================================
ownership ['First Owner', 'Second Owner', 'Third Owner']
Categories (3, object): ['First Owner', 'Second Owner', 'Third Owner']
====================================================================================================
transmission ['Automatic', 'Manual']
Categories (2, object): ['Automatic', 'Manual']
====================================================================================================
df.dtypes
insurance_validity     category
fuel_type              category
seats                     int64
kms_driven                int64
ownership              category
transmission           category
manufacturing_year        int64
mileage                 float64
engine                  float64
bhp                     float64
price                   float64
car_name_mean_price     float64
dtype: object
3-2. 카테고리 변수 더미화
df = pd.get_dummies(df, columns=categories, drop_first=True)
3-3. 수치형 변수 스케일링
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
num_features_extended = num_features + ["manufacturing_year"] + ["car_name_mean_price"]
df[num_features_extended] = scaler.fit_transform(df[num_features_extended])
3-4. 데이터셋 분리
from sklearn.model_selection import train_test_split
x_train, x_test, y_train, y_test = train_test_split(
    df.drop(target, axis=1), df[target], test_size=0.2, random_state=42
)
x_train.shape, x_test.shape, y_train.shape, y_test.shape
((764, 15), (192, 15), (764,), (192,))
3-5. 다중공선성 확인 및 제거
VIF값이 10 이상인 변수를 제거 합니다
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tools.tools import add_constant
df_with_const = add_constant(df)
vif = pd.DataFrame()
vif["Features"] = df_with_const.columns
vif["VIF"] = [
    variance_inflation_factor(df_with_const.values, i)
    for i in range(df_with_const.shape[1])
]
vif = (
    vif[vif["Features"] != "const"]
    .sort_values("VIF", ascending=False)
    .reset_index(drop=True)
)
vif[vif["VIF"] > 10]
| Features | VIF | |
|---|---|---|
| 0 | fuel_type_Petrol | 29.806683 | 
| 1 | fuel_type_Diesel | 28.621752 | 
| 2 | bhp | 10.949257 | 
df.drop(["fuel_type_Petrol", "bhp"], axis=1, inplace=True)
3-6. 선형회귀분석
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(x_train, y_train)
y_train_pred = lr.predict(x_train)
y_test_pred = lr.predict(x_test)
from sklearn.metrics import mean_squared_error, r2_score
print(f"Train RMSE: {mean_squared_error(y_train, y_train_pred):.2f}")
print(f"Test RMSE: {mean_squared_error(y_test, y_test_pred):.2f}")
print(f"Train R2: {r2_score(y_train, y_train_pred):.2f}")
print(f"Test R2: {r2_score(y_test, y_test_pred):.2f}")
Train RMSE: 46.44
Test RMSE: 58.93
Train R2: 0.83
Test R2: 0.82
많은 이상치와 중복데이터 그리고 결측치 등 데이터셋이 클린하지 않아서 전처리 과정에 많은 시간이 소요되었습니다.
그러나 선형회귀분석 결과는 Train RMSE: 46.44, Test RMSE: 58.93, Train R2: 0.83, Test R2: 0.82로 나쁘지 않은 결과를 보여줍니다.
      
      
      
      
      
Leave a comment