3.3. 선형회귀분석을 위한 준비#

이 절에서는 선형회귀분석을 하기 위한 준비작업을 설명한다. 준비작업은 크게 다음과 같다.

  1. 변수의 결정

  2. 데이터의 수집

  3. 데이터의 변환

  4. 데이터 전처리

이 절에서는 지금까지 계속 사용해온 팁 데이터 예제를 통해 이러한 준비 과정에 대해 설명한다.

3.3.1. 변수의 결정#

회귀분석을 하기 위한 가장 첫번째 단계는 종속변수와 독립변수를 결정하는 것이다. 즉, 어떤 데이터(종속변수)에 영향을 주는 요소가 무엇무엇(독립변수)이 있을지 찾아내야 한다. 보통 분석의 대상이 되는 종속변수는 결정되어 있는 경우가 많지만 영향을 비치는 독립변수는 분석을 수행하는 사람이 찾아야 한다. 당연하게도 목표가 되는 종속변수에 중요한 영향을 끼지는 핵심 독립변수들을 찾지 못하면 좋은 분석결과는 나올 수 없다. 이 부분은 컴퓨터나 수학이 대신해 줄 수 없는 부분으로 데이터가 나온 분야의 지식과 노하우 그리고 직관에 의해 결정된다.

변수가 결정되면 결정된 종속변수를 \(y\), 결정된 \(K\)개의 독립변수를 \(x_1, x_2, \ldots, x_K\)라고 이름붙인다.

우리는 팁 데이터에서 팁금액(tip)을 종속변수 \(y\)로, 수치형 데이터인 지불금액(total_bill)과 고객인원수(size) 그리고 성별(sex)을 각각 독립변수 \(x_1\), \(x_2\), \(x_3\)로 정하기로 하자.

  • 독립변수

    • \(x_1\): total_bill

    • \(x_2\): size

    • \(x_3\): sex

  • 종속변수

    • \(y\): total_bill

독립변수는 나중에 설명할 모형 조정(tuning) 단계에서 변경될 수 있다.

3.3.2. 데이터의 수집#

실제 데이터 분석 실무에서는 데이터의 수집 과정에 들어가는 시간과 노력이 대부분을 차지할 정도로 중요한 과정이나 여기에서는 이미 수집된 데이터가 존재하고 load_dataset 함수로 간단하게 얻을 수 있다.

데이터는 보통 다음과 같은 형식으로 수집된다. 여기에서는 수집된 데이터의 갯수가 \(N\)개라고 가정한다. \(i\)번째 레코드의 종속변수 값을 \(y_i\), \(i\)번째 레코드의 \(j\)번째 독립변수 값을 \(x_{ij}\)라고 표시한다.

종속변수 \(y\)

독립변수 \(x_1\)

독립변수 \(x_2\)

표본 1

수치 \(y_1\)

수치 \(x_{11}\)

수치 \(x_{12}\)

표본 2

수치 \(y_2\)

수치 \(x_{21}\)

수치 \(x_{22}\)

표본 3

수치 \(y_3\)

수치 \(x_{31}\)

수치 \(x_{32}\)

:

:

:

:

표본 N

수치 \(y_N\)

수치 \(x_{N1}\)

수치 \(x_{N2}\)

이 과정은 원래 현실세계에서 데이터를 수집하는 과정이 필요하지만 여기에서는

import seaborn as sns

tips = sns.load_dataset("tips")
tips
total_bill tip sex smoker day time size
0 16.99 1.01 Female No Sun Dinner 2
1 10.34 1.66 Male No Sun Dinner 3
2 21.01 3.50 Male No Sun Dinner 3
3 23.68 3.31 Male No Sun Dinner 2
4 24.59 3.61 Female No Sun Dinner 4
... ... ... ... ... ... ... ...
239 29.03 5.92 Male No Sat Dinner 3
240 27.18 2.00 Female Yes Sat Dinner 2
241 22.67 2.00 Male Yes Sat Dinner 2
242 17.82 1.75 Male No Sat Dinner 2
243 18.78 3.00 Female No Thur Dinner 2

244 rows × 7 columns

3.3.3. 데이터를 벡터와 행렬 형식으로 변환#

데이터가 준비되면 다음과 같이

  • 종속변수는 벡터 \(y\) 형식으로

  • 독립변수는 행렬 \(X\) 형식으로

변환한다.

우선 종속변수인 \(y_i\)값을 모아서 종속변수벡터 \(y\)를 만든다.

\[\begin{split} \mathbf{y} = \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_N \\ \end{bmatrix} \end{split}\]

다음으로 \(N\)개의 \(j\)번째 독립변수 \(x_{ij}\) 데이터를 모아서 독립변수벡터 \(x_j\)를 만든다.

\[\begin{split} x_j = \begin{bmatrix} x_{1,j} \\ x_{2,j} \\ \vdots \\ x_{N,j} \\ \end{bmatrix} \end{split}\]

이러한 과정을 \(j=1,\ldots,K\)에 대해 반복하여 \(K\)개의 독립변수 벡터 \(x_1, x_2, \ldots, x_K\)를 만들고 이를 모아서 독립변수행렬 \(X\)를 만든다. 독립변수행렬은 실험설계행렬(experiment design matrix) 혹은 설계행렬(design matrix)이라고도 부른다.

\[\begin{split} X = \begin{bmatrix} x_1 & x_2 & \cdots & x_K \end{bmatrix} = \begin{bmatrix} x_{11} & x_{12} & \cdots & x_{1K} \\ x_{21} & x_{22} & \cdots & x_{2K} \\ \vdots & \vdots & \ddots & \vdots \\ x_{N1} & x_{N2} & \cdots & x_{NK} \\ \end{bmatrix} \end{split}\]

이렇게 데이터를 벡터와 행렬로 만들면 선형회귀분석 모형

\[ y = w_1 \cdot x_1 + w_2 \cdot x_2 + \cdots + w_K \cdot x_K \]

을 다음과 같이 간단한 행렬곱으로 표시할 수 있다.

\[\begin{split} \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_N \end{bmatrix} = \begin{bmatrix} x_{11} & x_{12} & \cdots & x_{1K} \\ x_{21} & x_{22} & \cdots & x_{2K} \\ \vdots & \vdots & \ddots & \vdots \\ x_{N1} & x_{N2} & \cdots & x_{NK} \\ \end{bmatrix} \begin{bmatrix} w_1 \\ w_2 \\ \vdots \\ w_K \end{bmatrix} \end{split}\]

여기에서 가중치 \(w_j\)로 이루어진 가중치벡터 \(w\)를 다음과 같이 정의하면

\[\begin{split} w = \begin{bmatrix} w_1 \\ w_2 \\ \vdots \\ w_K \end{bmatrix} \end{split}\]

전체 방정식은 행렬과 벡터의 곱으로 간단하게 표기할 수 있다.

\[ y = X w \]

팁 데이터에 대해서는 \(y, X\)를 다음과 같이 구한다.

y = tips["tip"]
y
0      1.01
1      1.66
2      3.50
3      3.31
4      3.61
       ... 
239    5.92
240    2.00
241    2.00
242    1.75
243    3.00
Name: tip, Length: 244, dtype: float64
X = tips[["total_bill", "size", "sex"]]
X
total_bill size sex
0 16.99 2 Female
1 10.34 3 Male
2 21.01 3 Male
3 23.68 2 Male
4 24.59 4 Female
... ... ... ...
239 29.03 3 Male
240 27.18 2 Female
241 22.67 2 Male
242 17.82 2 Male
243 18.78 2 Female

244 rows × 3 columns

3.3.4. 데이터의 전처리#

이렇게 수집 및 정리된 데이터 값에 대해서는 다음과 같은 전처리를 해야한다.

  • 잘못되거나 누락된 데이터가 있는지 검사하고 해당 레코드 삭제하거나 정상적인 데이터로 대체

  • 상수항 추가

  • 범주형 독립변수 인코딩

  • 수치형 독립변수 스케일링

3.3.4.1. 잘못되거나 누락된 데이터 처리#

만약 데이터 중에 잘못되거나 누락된 데이터가 있어면 해당 레코드(행)나 해당 필드(열)을 삭제하거나 가장 그럴듯한 값으로 대체할 필요가 있다. 가장 그럴듯한 값으로 대체하는 것을 결측값대치(missing value imputation)라고 한다. 삭제하는 것은 간단하지만 결측값대치는 사전에 고려해야할 사항과 방법론이 많다. 가장 간단한 결측값대치 방법은 다음과 같다.

  • 범주형 데이터의 경우 해당 열의 최빈값(mode)으로 대치한다.

  • 수치형 데이터의 경우 해당 열의 평균값(mean) 혹은 중앙값(median)으로 대치한다.

기타 다른 결측값대치 방법에 대해서는 추후에 자세히 설명한다. 팁 데이터의 경우에는 결측치가 없으므로 이 부분은 생략한다.

3.3.4.2. 상수항 추가#

위에서 사용한 선형회귀분석모형

\[ y = w_1 \cdot x_1 + w_2 \cdot x_2 + \cdots + w_K \cdot x_K \]

에는 상수항(constance, intercept) \(w_0\)가 빠져있다. 이 값을 넣은 정상적인 선형회귀분석모형

\[ y = w_0 + w_1 \cdot x_1 + w_2 \cdot x_2 + \cdots + w_K \cdot x_K \]

을 사용하려면 위에서 구한 독립변수행렬에 상수항 열을 하나 추가해야 한다.

\[\begin{split} \begin{bmatrix} y_1 \\ y_2 \\ \vdots \\ y_N \end{bmatrix} = \begin{bmatrix} 1 & x_{11} & x_{12} & \cdots & x_{1K} \\ 1 & x_{21} & x_{22} & \cdots & x_{2K} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_{N1} & x_{N2} & \cdots & x_{NK} \\ \end{bmatrix} \begin{bmatrix} w_0 \\ w_1 \\ w_2 \\ \vdots \\ w_K \end{bmatrix} \end{split}\]

따라서 독립변수행렬은 다음과 같아지고

\[\begin{split} X = \begin{bmatrix} 1 & x_{11} & x_{12} & \cdots & x_{1K} \\ 1 & x_{21} & x_{22} & \cdots & x_{2K} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & x_{N1} & x_{N2} & \cdots & x_{NK} \\ \end{bmatrix} \end{split}\]

가중치 벡터 \(w\)는 다음과 같아진다.

\[\begin{split} w = \begin{bmatrix} w_0 \\ w_1 \\ w_2 \\ \vdots \\ w_K \end{bmatrix} \end{split}\]

이를 상수항 추가(constant augmentation)라고 한다.

statsmodels 패키지의 add_constant 함수를 사용하면 쉽게 상수항 추가를 할 수 있다. 하지만 다음 소절에서 설명할 patsy 패키지를 사용하면 상수항 추가와 범주값 처리를 쉽게 할 수 있다.

import statsmodels.api as sm

sm.add_constant(X)
const total_bill size sex
0 1.0 16.99 2 Female
1 1.0 10.34 3 Male
2 1.0 21.01 3 Male
3 1.0 23.68 2 Male
4 1.0 24.59 4 Female
... ... ... ... ...
239 1.0 29.03 3 Male
240 1.0 27.18 2 Female
241 1.0 22.67 2 Male
242 1.0 17.82 2 Male
243 1.0 18.78 2 Female

244 rows × 4 columns

3.3.4.3. 범주형 독립변수 인코딩#

팁 데이터의 독립변수 중에서 sex 데이터는 FemaleMale이라는 두 종류의 문자열로 이루어진 범주형 값이다. 범주형 값은 선형회귀분석모형

\[ y = w_0 + w_1 \cdot x_1 + w_2 \cdot x_2 + \cdots + w_K \cdot x_K \]

에 직접 넣을 수 없으므로 수치형 값으로 바꾸는 과정이 필요하다.

sex와 같이 두 종류의 값만 가지는 범주형 값인 경우 일반적으로 하나의 값을 0으로 다른 하나의 값을 1로 바꾸는 것이 보통이다. 두 종류가 넘는 값을 가지는 범주형 값인 경우 여러가지 방법으로 수치값 변환을 할 수 있다. 이러한 과정을 범주값 인코딩(category encoding)이라고 하며 이 주제에 대해서는 추후 자세히 설명한다.

patsy 패키지의 dmatrix 함수를 사용하면 이러한 범주값 인코딩을 쉽게 할 수 있다. dmatrix라는 이름은 설계행렬(design matrix)의 축약어로부터 왔다.

from patsy import dmatrix

dmatrix 함수는 모형 문자열과 독립변수행렬의 두가지 입력변수를 가진다.

  • 첫번째 입력변수 : 모형 문자열

  • 두번째 입력변수 : 독립변수행렬

모형문자열(formula string)은 독립변수 이름을 나열한 문자열이다. 독립변수와 독립변수 사이에는 + 기호를 넣어야 한다. 띄어쓰기는 무시되므로 아무렇게나 넣어도 된다. 예를 들어 팁 데이터 전체가 들어있는 tips 데이터프레임에서 total_bill과 size만 독립변수로 사용하는 경우 모형 문자열은 다음과 같아진다.

"total_bill + size"

이 때 독립변수행렬은 다음과 같이 얻을 수 있다. 결과는 DesignMatrix 클래스의 인스턴스가 된다. 또한 상수항은 Intercept 라는 이름으로 자동으로 추가된다.

dmatrix("total_bill + size", tips)
DesignMatrix with shape (244, 3)
  Intercept  total_bill  size
          1       16.99     2
          1       10.34     3
          1       21.01     3
          1       23.68     2
          1       24.59     4
          1       25.29     4
          1        8.77     2
          1       26.88     4
          1       15.04     2
          1       14.78     2
          1       10.27     2
          1       35.26     4
          1       15.42     2
          1       18.43     4
          1       14.83     2
          1       21.58     2
          1       10.33     3
          1       16.29     3
          1       16.97     3
          1       20.65     3
          1       17.92     2
          1       20.29     2
          1       15.77     2
          1       39.42     4
          1       19.82     2
          1       17.81     4
          1       13.37     2
          1       12.69     2
          1       21.70     2
          1       19.65     2
  [214 rows omitted]
  Terms:
    'Intercept' (column 0)
    'total_bill' (column 1)
    'size' (column 2)
  (to view full data, use np.asarray(this_obj))

여기에 sex 값을 독립변수로 추가하려면 모형 문자열을 다음과 같이 바꾸면 된다.

"total_bill + size + sex"

dmatrix 함수를 사용하면 문자열로 구성된 데이터 열은 자동으로 범주형 값으로 간주하여 인코딩을 해준다.

dmatrix("total_bill + size + sex", tips)
DesignMatrix with shape (244, 4)
  Intercept  sex[T.Female]  total_bill  size
          1              1       16.99     2
          1              0       10.34     3
          1              0       21.01     3
          1              0       23.68     2
          1              1       24.59     4
          1              0       25.29     4
          1              0        8.77     2
          1              0       26.88     4
          1              0       15.04     2
          1              0       14.78     2
          1              0       10.27     2
          1              1       35.26     4
          1              0       15.42     2
          1              0       18.43     4
          1              1       14.83     2
          1              0       21.58     2
          1              1       10.33     3
          1              0       16.29     3
          1              1       16.97     3
          1              0       20.65     3
          1              0       17.92     2
          1              1       20.29     2
          1              1       15.77     2
          1              0       39.42     4
          1              0       19.82     2
          1              0       17.81     4
          1              0       13.37     2
          1              0       12.69     2
          1              0       21.70     2
          1              1       19.65     2
  [214 rows omitted]
  Terms:
    'Intercept' (column 0)
    'sex' (column 1)
    'total_bill' (column 2)
    'size' (column 3)
  (to view full data, use np.asarray(this_obj))

만약 범주형 데이터이지만 수치형으로 되어 있거나 명시적으로 범주형이라는 것을 표시하고 싶으면 해당 독립변수 이름에 C()라는 함수를 적용하여 모형 문자열을 다음과 같이 표시한다.

"total_bill + size + C(sex)"
dmatrix("total_bill + size + C(sex)", tips)
DesignMatrix with shape (244, 4)
  Intercept  C(sex)[T.Female]  total_bill  size
          1                 1       16.99     2
          1                 0       10.34     3
          1                 0       21.01     3
          1                 0       23.68     2
          1                 1       24.59     4
          1                 0       25.29     4
          1                 0        8.77     2
          1                 0       26.88     4
          1                 0       15.04     2
          1                 0       14.78     2
          1                 0       10.27     2
          1                 1       35.26     4
          1                 0       15.42     2
          1                 0       18.43     4
          1                 1       14.83     2
          1                 0       21.58     2
          1                 1       10.33     3
          1                 0       16.29     3
          1                 1       16.97     3
          1                 0       20.65     3
          1                 0       17.92     2
          1                 1       20.29     2
          1                 1       15.77     2
          1                 0       39.42     4
          1                 0       19.82     2
          1                 0       17.81     4
          1                 0       13.37     2
          1                 0       12.69     2
          1                 0       21.70     2
          1                 1       19.65     2
  [214 rows omitted]
  Terms:
    'Intercept' (column 0)
    'C(sex)' (column 1)
    'total_bill' (column 2)
    'size' (column 3)
  (to view full data, use np.asarray(this_obj))

이 때 인코딩된 sex 열의 이름은 C(sex)[T.Female]이라는 이름으로 바뀌는데 이는 인코딩된 값이고 Female이라는 문자열을 1이라는 수치로 인코딩했다는 의미이다.

3.3.4.4. 수치형 독립변수 스케일링#

수치형 독립변수의 경우 사용하는 데이터의 단위에 따라 수치의 크기가 달라질 수 있다. 예를 들어 total_bill의 원래 단위는 달러(dollar)이지만 만약 센트(cent)를 단위로 쓴다면 100배가 커진 값이 될 것이다.

그런데 수치형 독립변수의 크기가 열마다 너무 크게 차이가 나면 회귀분석 결과에서 수치계산에 의한 오차가 발생할 수 있다. 이에 대한 자세한 내용은 추후에 데이터의 조건값(condition value)에 대해 설명하면서 같이 설명할 것이다.

이러한 오차를 방지하기 위해 모든 열의 수치형 독립변수의 크기를 비슷한 수준으로 맞추는 작업이 필요하다. 이를 수치형 독립변수의 스케일링(scaling) 작업이라고 한다. 스케일링 방법에도 여러가지가 있지만 가장 일반적으로 사용하는 스케일링 방법은 각 열의 평균이 0, 표준편차가 1이 되도록 각각의 열을 조정하는 방법이다.

예를 들어 팁 데이터의 경우 total_bill의 현재 평균과 표준편차는 약 19.786과 약 8.9다.

tips.total_bill.mean(), tips.total_bill.std()
(19.78594262295082, 8.902411954856856)

데이터의 평균이 0이 되고 표준편차가 1이 되도록 조정하려면 모든 total_bill 데이터 값에서 원래 평균인 19.786을 빼고 원래 표준편차인 8.9로 나누면 된다.

scaled_total_bill = (tips.total_bill - 19.786)/ tips.total_bill.std()
scaled_total_bill.mean().round(), scaled_total_bill.std()
(-0.0, 1.0)

patsy 패키지의 dmatrix 함수를 사용하는 경우 스케일링을 하고자 하는 독립변수 이름에 scale()이라고 쓰면 자동으로 스케일링을 해준다. 예를 들어 total_bill과 size 데이터를 스케일링하려면 모형 문자열은 다음과 같아진다.

"scale(total_bill) + scale(size) + C(sex)"
X = dmatrix("scale(total_bill) + scale(size) + C(sex)", tips)
X
DesignMatrix with shape (244, 4)
  Intercept  C(sex)[T.Female]  scale(total_bill)  scale(size)
          1                 1           -0.31471     -0.60019
          1                 0           -1.06324      0.45338
          1                 0            0.13778      0.45338
          1                 0            0.43832     -0.60019
          1                 1            0.54074      1.50696
          1                 0            0.61954      1.50696
          1                 0           -1.23995     -0.60019
          1                 0            0.79851      1.50696
          1                 0           -0.53420     -0.60019
          1                 0           -0.56347     -0.60019
          1                 0           -1.07111     -0.60019
          1                 1            1.74176      1.50696
          1                 0           -0.49143     -0.60019
          1                 0           -0.15262      1.50696
          1                 1           -0.55784     -0.60019
          1                 0            0.20194     -0.60019
          1                 1           -1.06436      0.45338
          1                 0           -0.39350      0.45338
          1                 1           -0.31696      0.45338
          1                 0            0.09726      0.45338
          1                 0           -0.21003     -0.60019
          1                 1            0.05674     -0.60019
          1                 1           -0.45203     -0.60019
          1                 0            2.21001      1.50696
          1                 0            0.00383     -0.60019
          1                 0           -0.22241      1.50696
          1                 0           -0.72218     -0.60019
          1                 0           -0.79872     -0.60019
          1                 0            0.21545     -0.60019
          1                 1           -0.01530     -0.60019
  [214 rows omitted]
  Terms:
    'Intercept' (column 0)
    'C(sex)' (column 1)
    'scale(total_bill)' (column 2)
    'scale(size)' (column 3)
  (to view full data, use np.asarray(this_obj))

스케일링을 할 때 주의할 점은 스케일링하는 방법 즉, total_bill을 8.9라는 숫자로 나누어주었다는 사실을 기억하고 있어야 한다는 점이다. 왜냐하면 나중에 우리가 구할 회귀분석모형은 이미 스케일링된 값을 사용하여 만들어진 함수이므로 예측에 사용할 때 total_bill 값을 그대로 넣으면 잘못된 값을 예측한다. 올바르게 예측하려면 total_bill을 8.9로 나누어서 회귀분석모형에 넣어야 한다.

DesignMatrix 클래스 인스턴스는 스케일링에 사용된 표준편차값들을 design_info 속성에 저장하고 있어서 나중에 사용할 수 있다.

예를 들어 다음과 같은 새로운 데이터가 있다고 가정하자. 19.786 + 8.9이라는 값은 평균인 19.786을 빼고 표준편차인 8.9로 나누면 약 1.0이 된다. 같은 방법으로 19.786은 0, 19.786 - 8.9은 -1이 될 것이다.

import pandas as pd

tips_new = pd.DataFrame(
    {"total_bill": [19.786 + 8.9, 19.786, 19.786 - 8.9], 
     "size": [3, 4, 5], 
     "sex": ["Female", "Male", "Female"]})
tips_new
total_bill size sex
0 28.686 3 Female
1 19.786 4 Male
2 10.886 5 Female

patsy 패키지의 build_design_matrices 함수와 design_info 속성을 사용하면 과거의 스케일링 정보를 사용하여 새 데이터를 스케일링한 결과를 얻을 수 있다.

from patsy import build_design_matrices

build_design_matrices([X.design_info], tips_new)
[DesignMatrix with shape (3, 4)
   Intercept  C(sex)[T.Female]  scale(total_bill)  scale(size)
           1                 1            1.00179      0.45338
           1                 0            0.00001      1.50696
           1                 1           -1.00178      2.56053
   Terms:
     'Intercept' (column 0)
     'C(sex)' (column 1)
     'scale(total_bill)' (column 2)
     'scale(size)' (column 3)]