선형회귀분석 - 중고차 가격에 영향을 미치는 요인 분석
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