Load packages
# numerical calculation & data frames
import numpy as np
import pandas as pd
# visualization
import matplotlib.pyplot as plt
import seaborn as sns
import seaborn.objects as so
# statistics
import statsmodels.api as sm
# pandas options
pd.set_option('mode.copy_on_write' , True ) # pandas 2.0
pd.options.display.float_format = ' {:.2f} ' .format # pd.reset_option('display.float_format')
pd.options.display.max_rows = 7 # max number of rows to display
# NumPy options
np.set_printoptions(precision = 2 , suppress= True ) # suppress scientific notation
# For high resolution display
import matplotlib_inline
matplotlib_inline.backend_inline.set_matplotlib_formats("retina" )
Why are low quality diamonds more expensive?
Load the dataset
diamonds = sm.datasets.get_rdataset("diamonds" , "ggplot2" ).data
# cut, color, clarity 모두 categorical type으로 변형
diamonds["cut" ] = pd.Categorical(
diamonds["cut" ],
categories= ["Fair" , "Good" , "Very Good" , "Premium" , "Ideal" ],
ordered= True
)
diamonds["color" ] = pd.Categorical(
diamonds["color" ],
categories= ["D" , "E" , "F" , "G" , "H" , "I" , "J" ],
ordered= True
)
diamonds["clarity" ] = pd.Categorical(
diamonds["clarity" ],
categories= ["I1" , "SI2" , "SI1" , "VS2" , "VS1" , "VVS2" , "VVS1" , "IF" ],
ordered= True
)
from sbcustom import rangeplot
cut = rangeplot(diamonds, x= "cut" , y= "price" )
color = rangeplot(diamonds, x= "color" , y= "price" )
clarity = rangeplot(diamonds, x= "clarity" , y= "price" )
Price and carat
다이아몬드의 퀄리티(cut, color, clarity)가 좋을수록 가벼워짐
price = (
so.Plot(diamonds, x= 'carat' , y= 'price' )
.add(so.Dots(alpha= .1 , color= ".6" ))
)
cut = rangeplot(diamonds, x= "cut" , y= "carat" )
color = rangeplot(diamonds, x= "color" , y= "carat" )
clarity = rangeplot(diamonds, x= "clarity" , y= "carat" )
다이아몬드의 퀄리티가 가격에 주는 영향/예측을 정확히 파악하기 위해 다음과 같은 절차를 통해 가격 대신 “캐럿으로 설명되지 않는 가격”(residuals)으로 종속변수를 대체함
우선, 2.5캐럿 이하로 제한하고,
가격과 캐럿을 log-transform하여 선형모형을 세움
이 모형으로 잔차를 구하고,
다이아몬드의 퀄리티와 이 잔차와의 관계를 살펴봄
diamonds2 = diamonds.query("carat < 2.5" ).assign(
lprice= lambda x: np.log2(x.price),
lcarat= lambda x: np.log2(x.carat)
)
log_plot = (
so.Plot(diamonds2, x= "lcarat" , y= "lprice" )
.add(so.Dots(color= ".6" , alpha= 0.1 ))
.add(so.Line(), so.PolyFit(5 ))
)
plot = (
so.Plot(diamonds2, x= "carat" , y= "price" )
.add(so.Dots(color= ".6" , alpha= 0.1 ))
.add(so.Line(), so.PolyFit(5 ))
)
# 캐럿으로 가격을 예측하는 선형모형
import statsmodels.formula.api as smf
mod_diamonds = smf.ols("lprice ~ lcarat" , data= diamonds2).fit()
# Parmeters
mod_diamonds.params
Intercept 12.19
lcarat 1.68
dtype: float64
Model: \(\displaystyle lprice = 1.68 \cdot lcarat + 12.19 + e\)
또는 \(\displaystyle \widehat{lprice} = 1.68 \cdot lcarat + 12.19\)
Prediction
# Data range from the carat variable
grid = pd.DataFrame({"carat" : []})
grid["carat" ]= np.linspace(diamonds2.carat.min (), diamonds2.carat.max (), 20 )
grid = grid.assign(
lcarat= lambda x: np.log2(x.carat), # 모형의 변수와 동일하게 log 변환
lprice= lambda x: mod_diamonds.predict(x.lcarat), # prediction
price= lambda x: 2 ** x.lprice # 원래 단위로 되돌리기
)
carat lcarat lprice price
0 0.20 -2.32 8.29 312.79
1 0.32 -1.64 9.43 691.47
2 0.44 -1.18 10.21 1182.86
.. ... ... ... ...
17 2.25 1.17 14.16 18318.33
18 2.37 1.24 14.29 19999.52
19 2.49 1.32 14.41 21740.08
[20 rows x 4 columns]
(
so.Plot(grid, x= "carat" , y= "price" )
.add(so.Line())
.add(so.Dots(color= ".6" , alpha= 0.1 ), x= diamonds2.carat, y= diamonds2.price)
)
캐럿과 가격은 비선형적인 관계에 있으며, 이를 log-transform하여 선형적인 관계로 만들어줌
또한, variability는 캐럿이 증가함에 따라 비례해서 커지는 양상을 보임; 이 또한 log-transform을 통해 해결되었음
Residuals 분석
위 모형은 충분히 좋은가?
diamonds2["lresid" ] = mod_diamonds.resid
(
so.Plot(diamonds2, x= 'lcarat' , y= 'lresid' )
.add(so.Dots(alpha= .1 ))
)
이제, 다이아몬드의 퀄리티와 위에서 구한 가격의 residuals과의 관계를 살펴보면,
y축은 log2 scale로 변환된 것이므로, 원래 단위로 이해하면,
residual +1은 캐럿으로 예측되는 가격(residual = 0)보다 가격이 2배 비싸다는 것을 의미
residual -1은 캐럿으로 예측되는 가격(residual = 0)보다 가격이 1/2배 낮다는 것을 의미
이는 캐럿의 영향을 고려한 후에 , 다이아몬드의 퀄리티 각각이 가격에 (상대적으로) 얼마나 영향을 주는지를 가늠할 수 있음
cut = rangeplot(diamonds2, x= "cut" , y= "lresid" )
color = rangeplot(diamonds2, x= "color" , y= "lresid" )
clarity = rangeplot(diamonds2, x= "clarity" , y= "lresid" )
mod1 = smf.ols("lprice ~ lcarat + cut" , data= diamonds2).fit()
mod2 = smf.ols("lprice ~ lcarat + color" , data= diamonds2).fit()
mod3 = smf.ols("lprice ~ lcarat + clarity" , data= diamonds2).fit()
# mod.params
Intercept 11.84
cut[T.Good] 0.23
cut[T.Very Good] 0.34
cut[T.Premium] 0.33
cut[T.Ideal] 0.45
lcarat 1.70
dtype: float64
Intercept 12.37
color[T.E] -0.04
color[T.F] -0.05
color[T.G] -0.08
color[T.H] -0.27
color[T.I] -0.41
color[T.J] -0.61
lcarat 1.73
dtype: float64
Intercept 11.24
clarity[T.SI2] 0.66
clarity[T.SI1] 0.87
clarity[T.VS2] 1.08
clarity[T.VS1] 1.15
clarity[T.VVS2] 1.38
clarity[T.VVS1] 1.45
clarity[T.IF] 1.58
lcarat 1.81
dtype: float64
다이아몬드의 3가지 퀄리티가 서로 연관되어 있다면?
A more complicated model
다이아몬드의 3가지 퀄리티와 carat이 모두 연관되어 있어, 각각의 고유한 효과를 보기 위해 다음과 같이 모든 예측변수들을 포함하는 모형을 세울 수 있음
mod_full = smf.ols('lprice ~ lcarat + cut + color + clarity' , data= diamonds2).fit()
Prediction
첫번째로 이 모형에 의한, cut에 따른 예측값을 구하기 위해, cut을 제외한 다른 값들을 고정한 grid를 구해보면,
연속변수에 대해서는 보통 평균(mean)이나 중앙값(median)을
카테고리 변수에 대해서는 보통 최빈값(mode)을 사용
grid = pd.DataFrame({"cut" : ["Fair" , "Good" , "Very Good" , "Premium" , "Ideal" ]})
grid["color" ] = diamonds2.color.mode()[0 ] # mode: 최빈값
grid["clarity" ] = diamonds2.clarity.mode()[0 ]
grid["lcarat" ] = diamonds2.lcarat.median()
cut color clarity lcarat
0 Fair G SI1 -0.51
1 Good G SI1 -0.51
2 Very Good G SI1 -0.51
3 Premium G SI1 -0.51
4 Ideal G SI1 -0.51
즉, G 컬러이고, SI1의 투명도와, 캐럿(로그) -0.51인 다이아몬드에 대해서, cut이 좋아질수록 가격이 얼마나 올라가는지를 예측해 본다면,
grid["lpred" ] = mod_full.predict(grid)
grid["pred" ] = 2 ** grid.lpred # 원래 단위인 price로 되돌리기
grid
cut color clarity lcarat lpred pred
0 Fair G SI1 -0.51 10.99 2035.36
1 Good G SI1 -0.51 11.10 2202.21
2 Very Good G SI1 -0.51 11.16 2285.37
3 Premium G SI1 -0.51 11.19 2337.24
4 Ideal G SI1 -0.51 11.22 2388.52
(
so.Plot(grid, x= 'cut' , y= 'pred' )
.add(so.Line(marker= "o" ))
.limit(y= (1300 , 4100 )) # 아래 플랏과 비교하기 위해 y축 범위를 고정
).show()
# 모형의 파라미터를 해석해 보면
2 ** mod_full.params[1 :5 ]
cut[T.Good] 1.08
cut[T.Very Good] 1.12
cut[T.Premium] 1.15
cut[T.Ideal] 1.17
dtype: float64
cut 대신 color와 clarity에 대해서도 그려볼 것
Residuals 분석
diamonds2["lresid_full" ] = mod_full.resid
(
so.Plot(diamonds2, x= 'lcarat' , y= 'lresid_full' )
.add(so.Dots(alpha= .1 ))
)
이상치들만 자세히 들여다보면,
from numpy import abs
diamonds2.query("@abs(lresid_full) > 1" ).assign(
pred_full= lambda x: 2 ** mod_full.predict(x[["lcarat" , "cut" , "color" , "clarity" ]]),
resid_full= lambda x: x.price - x.pred_full,
).sort_values("resid_full" )
carat cut color clarity depth table price x y z \
22440 2.46 Premium E SI2 59.70 59.00 10470 8.82 8.76 5.25
41918 1.03 Fair E I1 78.20 54.00 1262 5.72 5.59 4.42
38153 0.25 Fair F SI2 54.40 64.00 1013 4.30 4.23 2.32
... ... ... ... ... ... ... ... ... ... ...
5325 0.61 Good F SI2 62.50 65.00 3807 5.36 5.29 3.33
8203 0.51 Fair F VVS2 60.70 66.00 4368 5.21 5.11 3.13
21935 1.01 Fair D SI2 64.60 58.00 10011 6.25 6.20 4.02
lprice lcarat lresid lresid_full pred_full resid_full
22440 13.35 1.30 -1.02 -1.17 23630.26 -13160.26
41918 10.30 0.04 -1.96 -1.07 2650.65 -1388.65
38153 9.98 -2.00 1.15 1.94 264.51 748.49
... ... ... ... ... ... ...
5325 11.89 -0.71 0.90 1.31 1539.74 2267.26
8203 12.09 -0.97 1.53 1.36 1706.07 2661.93
21935 13.29 0.01 1.07 1.30 4052.40 5958.60
[16 rows x 16 columns]
좀 더 체계적으로 다음과 같이 모형의 복잡성 이 올라감에 따라 예측의 정확성이 어떻게 변하는지 알아보면
diamonds2 = diamonds.query("carat < 2.5" ).assign(
lprice= lambda x: np.log2(x.price),
lcarat= lambda x: np.log2(x.carat)
)
# Nested models
diamonds2_mod1 = smf.ols("lprice ~ lcarat" , data= diamonds2).fit()
diamonds2_mod2 = smf.ols("lprice ~ lcarat + clarity" , data= diamonds2).fit()
diamonds2_mod3 = smf.ols("lprice ~ lcarat + cut + color + clarity" , data= diamonds2).fit()
code
diamonds2_mods = diamonds2.assign(
mod1= diamonds2_mod1.resid,
mod2= diamonds2_mod2.resid,
mod3= diamonds2_mod3.resid,
)
diamonds2_mods = diamonds2_mods.melt(
id_vars= ["lcarat" , "lprice" ],
value_vars= ["mod1" , "mod2" , "mod3" ],
var_name= "model" ,
value_name= "resid" ,
)
(
so.Plot(diamonds2_mods, x= 'lcarat' , y= 'resid' , color= 'model' )
.add(so.Dots(alpha= .1 ))
.facet("model" )
.layout(size= (8 , 5 ))
)
code
(
so.Plot(diamonds2_mods, x= 'resid' , color= 'model' )
.add(so.Line(), so.Hist(bins= 50 ))
.layout(size= (5 , 3 ))
)
code
from statsmodels.tools.eval_measures import rmse, meanabs
mods = [diamonds2_mod1, diamonds2_mod2, diamonds2_mod3]
y = diamonds2.price
print ("The prediction accuracy of the models (original unit except R-squared): \n " )
for i, mod in enumerate (mods):
y_hat = 2 ** mod.fittedvalues
R2 = np.var(y_hat) / np.var(y)
R2_log = mod.rsquared
print (
f"Model { i+ 1 } >> R-squared(log): { R2_log:.2f} , R_squared { R2:.2f} , "
f"RMSE: { rmse(y, y_hat):.2f} , MAE: { meanabs(y, y_hat):.2f} "
)
The prediction accuracy of the models (original unit except R-squared):
Model 1 >> R-squared(log): 0.93, R_squared 0.88, RMSE: 1507.04, MAE:817.34
Model 2 >> R-squared(log): 0.97, R_squared 0.98, RMSE: 1164.81, MAE:623.65
Model 3 >> R-squared(log): 0.98, R_squared 0.95, RMSE: 732.70, MAE:390.61
The prediction accuracy of the models can be evaluated by the following metrics:
(The strength of association )
변량의 비율로 해석하고 싶다면,
\(\displaystyle\frac{V(predictions)}{V(Y)} + \frac{V(residuals)}{V(Y)} = 1,\) (OLS estimate)
즉, “모형에 의해 설명되는 \(Y\) 변량의 비율 ” + “모형에 의해 설명되지 않는 \(Y\) 변량의 비율 ” = 1
첫 항을 \(R^2\) 라고 하고, 결정계수 혹은 R squared라고 부름
따라서, \(1-R^2\) 는 설명되지 않는 변량의 비율이라고 할 수 있음.
\(또한, R\) 은 multiple correlation coefficient라고 부르는데, 이는 \(Y\) 와 예측값 \(\hat Y\) 의 상관계수(Pearson’s correlation coefficient)를 의미함.
비율이 아닌 \(Y\) 의 단위와 동일한 단위로 해석하고 싶다면,
Root-mean-squared deviation/error: \(RMSE = \displaystyle\sqrt{\frac{1}{n} \sum_{i=1}^{n}{(y_i -\hat y_i)^2}}\)
Mean absolute error: \(MAE = \displaystyle\frac{1}{n} \sum_{i=1}^{n}{|~y_i -\hat y_i~|}\) : 이상치에 덜 민감함
동일한 관계를 갖지만 예측정확성/설명력이 다른 경우의 예
Interactions
무게(carat)과 투명도(clarity)가 상호작용하여 가격에 영향을 준다는 가정하에, 즉, 투명도의 레벨에 따라 무게와 가격의 관계가 바뀔 수 있다는 가정
diamonds2_mod2 = smf.ols("lprice ~ lcarat + clarity" , data= diamonds2).fit()
diamonds2_mod2_interact = smf.ols("lprice ~ lcarat * clarity" , data= diamonds2).fit()
Prediction 비교
code
diamonds2_mods = diamonds2.assign(
pred_add= diamonds2_mod2.fittedvalues,
pred_interact= diamonds2_mod2_interact.fittedvalues,
)
diamonds2_mods = diamonds2_mods.melt(
id_vars= ["lcarat" , "lprice" , "clarity" ],
value_vars= ["pred_add" , "pred_interact" ],
var_name= "model" ,
value_name= "pred" ,
)
(
so.Plot(diamonds2_mods, x= 'lcarat' , y= 'pred' , color= 'clarity' )
.add(so.Line())
.scale(color= so.Nominal(order= diamonds2.clarity.cat.categories.tolist()))
.facet("model" )
.layout(size= (8 , 4.5 ))
)
Residuals 비교
code
diamonds2_mods = diamonds2.assign(
resid_add= diamonds2_mod2.resid,
resid_interact= diamonds2_mod2_interact.resid,
)
diamonds2_mods = diamonds2_mods.melt(
id_vars= ["lcarat" , "lprice" , "clarity" ],
value_vars= ["resid_add" , "resid_interact" ],
var_name= "model" ,
value_name= "resid" ,
)
(
so.Plot(diamonds2_mods, x= "lcarat" , y= "resid" , color= "clarity" )
.add(so.Dots(alpha= 0.1 ))
.layout(size= (12 , 5 ))
.facet("clarity" , "model" )
)
두 가지 관점
모델 파라미터의 해석
변수와 변수간의 관계성에 초점
변수들을 “동시” 에 고려해서 봄으로써 각 변수들의 “고유한 impact”의 방향과 크기 를 해석하고자 함
ex. ols(lprice ~ lcarat + color + cut + clarity, data = diamonds2)
다이아몬드 투명도(clarity)의 레벨이 하나씩 올라감에 따라 가격형성에 어떻게 혹은 얼마나 영향을 주는가?
clarity[T.SI2] 0.59
clarity[T.SI1] 0.82
clarity[T.VS2] 1.04
...
clarity[T.VVS1] 1.44
clarity[T.IF] 1.58
lcarat 1.89
Length: 8, dtype: float64
또한, 변수간의 상호작용 을 고려하여, 각 변수들의 “고유한 impact”의 방향과 크기 에 대해 정교한 분석이 가능
lcarat 1.60
lcarat:clarity[T.SI2] 0.20
lcarat:clarity[T.SI1] 0.22
lcarat:clarity[T.VS2] 0.18
lcarat:clarity[T.VS1] 0.23
lcarat:clarity[T.VVS2] 0.25
lcarat:clarity[T.VVS1] 0.23
lcarat:clarity[T.IF] 0.27
dtype: float64
모델의 예측 정확성과 특성
Residuals의 분석
변수의 개수가 증가하면, 즉 모델이 복잡할수록 샘플에 대한 예측력은 높아짐.
하지만, 미래 데이터(모집단, 일반화)에 대한 예측력은 떨어질 수 있음.
한편, 변수들 간의 복잡한 correlation들이 모델의 해석에 오류를 가져올 수 있음.
Bias-Variance Tradeoff
Source: p.31, An Introduction to Statistical Learning (2e) by Gareth James, Daniela Witten, Trevor Hastie, and Robert Tibshirani
Exercises
샌프란시스코행 항공편의 도착은 왜 지연되었는가? nycflights13
Linear model: arr_delay ~ hour + origin + carrier + season + dow
# import the data
flights = sm.datasets.get_rdataset('flights' , 'nycflights13' ).data
# convert the date column to a datetime object
flights["time_hour" ] = pd.to_datetime(flights["time_hour" ])
# add a column for the day of the week
flights["dow" ] = (
flights["time_hour" ]
.dt.day_name() # 요일 추출
.str [:3 ] # 요일 앞 3글자
.astype("category" )
.cat.set_categories(["Sun" , "Mon" , "Tue" , "Wed" , "Thu" , "Fri" , "Sat" ])
)
# add a column for the season
flights["season" ] = np.where(flights["month" ].isin([6 , 7 ]), "summer" , "other month" )
# filter out the flights to SFO
sfo = flights.query('dest == "SFO" & arr_delay < 500' ).copy()
import statsmodels.formula.api as smf
mod = smf.ols("arr_delay ~ hour + origin + carrier + season + dow" , data= sfo).fit()
다음 데이터셋에서 집값에 미치는 변수들에 대해 그 영향력 살펴보세요.
Data on houses in Saratoga County, New York, USA in 2006
houses_data = sm.datasets.get_rdataset("SaratogaHouses" , "mosaicData" )
houses = houses_data.data # data
print (houses_data.__doc__) # description of the data
앞서 Model Basics 파트에서 다음과 같은 모형을 세웠는데, 그 적절성을 확인해보고, 상호작용 효과가 존재하는지를 포함해 이를 확장해보세요.
from statsmodels.formula.api import ols
houses["livingArea2" ] = houses["livingArea" ] / 35.5 # 1평으로 변환
mod_houses = ols("price ~ livingArea2 + bedrooms" , data= houses).fit()
Intercept 36667.90
livingArea2 4451.88
bedrooms -14196.77
dtype: float64
mod_houses.summary(slim= True )
OLS Regression Results
Dep. Variable:
price
R-squared:
0.515
Model:
OLS
Adj. R-squared:
0.515
No. Observations:
1728
F-statistic:
917.4
Covariance Type:
nonrobust
Prob (F-statistic):
4.31e-272
coef
std err
t
P>|t|
[0.025
0.975]
Intercept
3.667e+04
6610.293
5.547
0.000
2.37e+04
4.96e+04
livingArea2
4451.8772
125.210
35.555
0.000
4206.297
4697.457
bedrooms
-1.42e+04
2675.159
-5.307
0.000
-1.94e+04
-8949.872
Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.