지난번 포스팅에서는 분류 모델(classification model)의 성능을 평가할 수 있는 지표 (evaluation metrics)에 대해서 소개하였습니다. (https://rfriend.tistory.com/771)

 

이번 포스팅에서는 Python을 사용하여 예제 데이터에 대해 분류 모델의 성능 평가를 해보겠습니다. 

 

(1) 예제로 사용할 Breast Cancer 데이터셋을 로딩하고, Data와 Target으로 구분

(2) Training set, Test set 분할

(3) Logistic Regression 모델 적합 (Training)

(4) 예측 (Prediction)

(5) 혼돈 매트릭스 (Confusion Matrix)

(6) 분류 모델 성능 지표: Accuracy, Precision, Recall rate, Specificity, F-1 score

(7) ROC 곡선, AUC 점수

 

 

 

(1) 예제로 사용할 Breast Cancer 데이터셋을 로딩하고, Data와 Target으로 구분 

 

target의 '0' 이 'malignant (악성 종양)' 이고, '1'은 'benign' 으로서 정상을 의미합니다. 우리는 'malignant (악성 종양)' 을 분류하는 모델에 관심이 있으므로 target '0' 이 'Positive Label' 이 되겠습니다. 

 

import numpy as np

## load a Iris dataset
from sklearn.datasets import load_breast_cancer

bc = load_breast_cancer()

## target names
bc.target_names
#array(['malignant', 'benign'], dtype='<U9')

## [0: 'malignant'(악성), 1: 'benign']
np.unique(bc['target'], return_counts=True) 
#(array([0, 1]), array([212, 357]))


## getting data, target
bc_data = bc['data']
bc_target = bc['target']

bc_data.shape
#(569, 30)

bc_target[:20]
#array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])

 

 

(2) Training set, Test set 분할

 

Training set 0.5, Test set 0.5 의 비율로 무작위 추출하여 분할하였습니다. 

 

## splits training and test set
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    bc_data, 
    bc_target, 
    test_size=0.5,  
    random_state=1004
)

 

 

 

(3) Logistic Regression 모델 적합 (Training)

 

Traning set을 사용해서 로지스틱 회귀모형을 적합하였습니다.

 

## training a Logistic Regression model with training set
from sklearn.linear_model import LogisticRegression

clf = LogisticRegression(
    solver='liblinear', 
    random_state=1004).fit(X_train, y_train)

 

 

 

(4) 예측 (Prediction)

 

Test set에 대해서 예측을 하고 모델 성능 평가를 해보겠습니다. 

predict() 메소드는 범주를 예측하여 반환하고, predict_praba() 메소드는 확률(probability)을 반환합니다. 

 

## prediction for test set
y_pred = clf.predict(X_test) # class
y_pred_proba = clf.predict_proba(X_test) # probability

 

 

실제 범주의 값과 예측한 범주의 값, 그리고 target '0'(malignant, 악성 종양) 일 확률을 DataFrame으로 묶어보았습니다. 

 

# All in a DataFrame
pred_df = pd.DataFrame({
    'actual_class': y_test, 
    'predicted_class': y_pred, 
    'probabilty_class_0': y_pred_proba[:,0] # malignant (악성)
})

pred_df.head(10)
#    actual_class  predicted_class  probabilty_class_0
# 0             1                1            0.002951
# 1             0                0            0.993887
# 2             0                1            0.108006
# 3             1                1            0.041777
# 4             0                0            1.000000
# 5             0                0            1.000000
# 6             0                0            0.999633
# 7             1                1            0.026465
# 8             0                0            0.997405
# 9             1                1            0.002372

 

 

 

이제 여기서부터 분류 모델의 성능 평가를 시작합니다. 

 

(5) 혼돈 매트릭스 (Confusion Matrix)

 

혼돈 매트릭스의 Y축은 Actual 의 malignant (0), benign(1) 이며, X 축은 Predicted 의 malignant (0), benign(1) 입니다. 

 

## model evaluation
# Confusion matrix
from sklearn.metrics import confusion_matrix

confusion_matrix(y_test, y_pred, 
                 labels=[0, 1]) # 'malignant'(0), 'benign'(1)

#                   predicted
#                   malignant  benign
# actual malignant [[102,       16],
#        benign    [  3,       164]])

 

confusion matrix

 

 

 

(6) 분류 모델 성능 지표: Accuracy, Precision, Recall rate, Specificity, F-1 score

 

from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_recall_fscore_support

## performance metrics
accuracy = accuracy_score(y_test, y_pred)

precision, recall, fscore, support = \
    precision_recall_fscore_support(y_test, y_pred)

print('Accuracy   : %.3f' %accuracy) # (102+164)/(102+16+3+164)
print('Precision  : %.3f' %precision[0]) # 102/(102+3)
print('Recall     : %.3f' %recall[0]) # 102/(102+16)
print('Specificyty: %.3f' %recall[1]) # 164/(3+164)
print('F1-Score   : %.3f' %fscore[0]) # 2/(1/precision + 1/recall) = 2/(1/0.971+1/0.864)

# Accuracy   : 0.933
# Precision  : 0.971
# Recall     : 0.864
# Specificyty: 0.982
# F1-Score   : 0.915

 

 

sklearn의 classification_report() 메소드를 활용해서 한꺼번에 쉽게 위의 분류 모델 성능평가 지표를 계산하고 출력할 수 있습니다. 참고로, 'macro avg' 는 가중치가 없는 평균이며, 'weighted avg'는 support (관측치 개수) 로 가중치를 부여한 평균입니다. 

 

from sklearn.metrics import classification_report

target_names = ['malignant(0)', 'benign(1)']
print(classification_report(y_test, y_pred, 
                            target_names=target_names))

#               precision    recall  f1-score   support

# malignant(0)       0.97      0.86      0.91       118
#    benign(1)       0.91      0.98      0.95       167

#     accuracy                           0.93       285
#    macro avg       0.94      0.92      0.93       285
# weighted avg       0.94      0.93      0.93       285

 

 

 

 

(7) ROC 곡선, AUC 점수

 

ROC 곡선과 AUC 점수는 예측 확률을 이용합니다.

ROC 곡선은 모든 의사결정 기준선 (decision threshold)에 대하여 혼돈 매트릭스를 만들고, X축에는 False Positive Rate(=1-specificity), Y축에는 True Positive Rate (=recall, sensitivity) 의 값을 선그래프로 그린 것이며, 좌측 상단으로 그래프가 붙을 수록 더 잘 적합된 모델이라고 평가합니다. AUC 점수는 ROC 곡선의 아랫부분의 면적을 적분한 값입니다. 

 

## ROC Curve, AUC
import sklearn.metrics as metrics

fpr, tpr, threshold = metrics.roc_curve(
    y_test, 
    y_pred_proba[:, 0], 
    pos_label=0) # positive label

AUC = metrics.auc(fpr, tpr)

 

 

# plotting ROC Curve
import matplotlib.pyplot as plt

plt.figure(figsize = (8, 8))
plt.plot(fpr, tpr, 'b', label = 'AUC = %0.3f' % AUC)
plt.title(('ROC Curve of Logistic Regression'), fontsize=18)
plt.legend(loc = 'lower right')

plt.plot([0, 1], [0, 1],'r--') # random guess
plt.xlim([0, 1])
plt.ylim([0, 1])
plt.ylabel('True Positive Rate', 
                fontsize=14)
plt.xlabel('False Positive Rate', 
                fontsize=14)

plt.show()

ROC curve, AUC score

 

 

[ Reference ] 

1) Breast Cancer Dataset
: https://scikit-learn.org/stable/modules/generated/sklearn.datasets.load_breast_cancer.html

2) Scikit-Learn Logistic Regression
: https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html

3) 분류 모델의 성과 평가 지표 : https://rfriend.tistory.com/771

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

이번 포스팅에서는 여러개의 칼럼을 가지는 pandas DataFrame에서 특정 데이터 유형의 칼럼을 선택하거나 배제하는 방법을 소개하겠습니다. 

 

(1) pandas DataFrame 의 칼럼별 데이터 유형 확인: df.dtypes

(2) pandas DataFrame 에서 특정 데이터 유형의 칼럼을 선택하기: df.select_dtypes(include)

(3) pandas DataFrame 에서 특정 데이터 유형의 칼럼을 제외하기: df.select_dtypes(exclude)

 

 

pandas.DataFrame.select_dtypes(include, exclude)

 

 

먼저, 예제로 사용할 pandas DataFrame을 만들어보겠습니다. 데이터 유형으로는 int64, object, boolean, folat64, datetime64 의 5개 서로 다른 유형을 포함하도록 하였습니다. 

 

import pandas as pd

## sample pandas DataFrame
df = pd.DataFrame({
    'x1': [1, 2, 3], # int64
    'x2': ['a', 'b', 'c'], # object
    'x3': [True, False, False], # boolean
    'x4': [1.0, 2.0, 3.0], # float64
    'x5': [pd.Timestamp('20230101'), pd.Timestamp('20230102'), pd.Timestamp('20230103')] # datetime64
})


print(df)
#    x1 x2     x3   x4         x5
# 0   1  a   True  1.0 2023-01-01
# 1   2  b  False  2.0 2023-01-02
# 2   3  c  False  3.0 2023-01-03
# 3   4  d   True  4.0 2023-01-04

 

 

 

(1) pandas DataFrame 의 칼럼별 데이터 유형 확인: df.dtypes

 

## (1) pd.DataFrame.dtypes: data type of each column.

df.dtypes
# x1             int64
# x2            object
# x3              bool
# x4           float64
# x5    datetime64[ns]
# dtype: object

 

 

 

(2) pandas DataFrame 에서 특정 데이터 유형의 칼럼을 선택하기 (include)

 

pd.DataFrame.select_dtypes(include=None) 메소드를 사용하여 원하는 데이터 유형을 include = 'data type' 옵션에 넣어주면 됩니다. 아래 예시에서는 차례대로 include='int64', 'object', 'bool', float64', 'datetime64' 별로  칼럼을 선택해보았습니다. 

 

## (2) DataFrame.select_dtypes(include=None, exclude=None)
## Return a subset of the DataFrame’s columns based on the column dtypes.

## including the dtypes in include.
df.select_dtypes(include='int64') # int
# x1
# 0	1
# 1	2
# 2	3
# 3	4


df.select_dtypes(include='object')
# x2
# 0	a
# 1	b
# 2	c
# 3	d


df.select_dtypes(include='bool')
# x3
# 0	True
# 1	False
# 2	False
# 3	True


df.select_dtypes(include='float64') # float
# x4
# 0	1.0
# 1	2.0
# 2	3.0
# 3	4.0


df.select_dtypes(include='datetime64') # datetime
# x5
# 0	2023-01-01
# 1	2023-01-02
# 2	2023-01-03
# 3	2023-01-04

 

 

 

한꺼번에 여러개의 데이터 유형의 칼럼을 선택하고자 할 때는 df.select_dtypes(include=[dtype1, dtype2, ...]) 처럼 include 옵션에 여러개의 데이터 유형을 리스트로 넣어주면 됩니다. 아래 예시에서는 ['int64', 'float64'] 의 두 개의 숫자형 칼럼을 선택해 보았습니다. 

 

숫자형 (numeric data type) 의 칼럼을 선택하는 또 다른 방법은 df.select_dtypes(include='number') 를 해도 됩니다. 

 

## (a) include=[dtype, dtype, ...]
df.select_dtypes(include=['int64', 'float64']) # 여러개의 data type

# x1	x4
# 0	1	1.0
# 1	2	2.0
# 2	3	3.0
# 3	4	4.0



## (b) To select all numeric types, use np.number or 'number'
df.select_dtypes(include='number') # int, float

# x1	x4
# 0	1	1.0
# 1	2	2.0
# 2	3	3.0
# 3	4	4.0

 

 

숫자형('int64', 'float64') 의 칼럼 이름을 리스트로 반환하려면 columns 로 칼럼 이름을 가져와서 list() 로 묶어주면 됩니다. 

 

## column names of numeric types
list(df.select_dtypes(include='number').columns)

# ['x1', 'x4']

 

 

 

(3) pandas DataFrame 에서 특정 데이터 유형의 칼럼을 제외하기 (exclude)

 

위의 (2)번이 특정 데이터 유형을 포함(include)하는 칼럼을 선택하는 것이었다면, 이번에는 특정 데이터 유형을 제외(exclude) 한 나머지 칼럼을 선택하는 방법입니다. 아래 예시에서는 'int64' 데이터 유형을 제외(exclude='int64')한 나머지 칼럼을 반환하였습니다. 

 

## excluding the dtypes in exclude.
df.select_dtypes(exclude='int64')

# x2	x3	x4	x5
# 0	a	True	1.0	2023-01-01
# 1	b	False	2.0	2023-01-02
# 2	c	False	3.0	2023-01-03
# 3	d	True	4.0	2023-01-04

 

 

 

[Reference]

pandas.DataFrame.select_dtypes(include=None, exclude=None)
: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.select_dtypes.html

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

Python 리스트 자료형의 메소드와 내장함수에 대해서는 https://rfriend.tistory.com/330 를 참고하세요. 

 

이번 포스팅에서는 리스트(List) 자료형에 대한 유용한 활용 팁 네가지를 소개하려고 합니다. 

 

(1) 리스트의 문자형 원소를 숫자형 원소로 바꾸기 (혹은 그 반대)

(2) 리스트의 원소를 사전형의 Key:Value 기준으로 매핑하여 변환하기

(3) 리스트에서 또 다른 리스트의 겹치는 원소를 빼기

(4) 리스트 원소 정렬하기 (내림차순, 오름차순)

 

 

(1) 리스트의 문자형 원소를 숫자형 원소로 바꾸기 (혹은 그 반대)

 

list(map(data type, list)) 으로 리스트 내 원소의 데이터 유형을 변환할 수 있습니다. 아래는 순서대로 리스트 내 문자형 원소를 숫자형으로 변환, 숫자형 원소를 문자형 원소로 변환한 예입니다. 

 

## convert a list with string-type elements into a list with numeric-type elements
list(map(int, ['1', '2', '3']))  # 문자형 원소
# [1, 2, 3] # --> 숫자형으로 변환됨


## convert a list with numeric-type elements into a list with string-type elements
list(map(str, [1, 2, 3]))  # 숫자형 원소
# ['1', '2', '3'] # --> 문자형으로 변환됨

 

 

 

(2) 리스트의 원소를 사전형의 Key:Value 기준으로 매핑하여 변환하기

 

리스트 내 원소를 다른 값으로 변환할 때 사전형(Dictionary)의 Key:Value 매핑을 이용하면 편리합니다. List Comprehension 을 이용해서 리스트 원소별로 for loop 을 돌면서 Dictionary 의 Dict[Key] 로 Value에 접근해서 키별로 값을 매핑해서 변환된 값으로 새로운 리스트를 만들어줍니다

 

converting elements in a list using Dictionary(Key: Value)

 

## a List
my_list = ['c', 'a', 'd', 'b']


## a Dictionary, which will be used for mapping, converting
my_dict = {
    'a': 1, 
    'b': 2, 
    'c': 3, 
    'd': 4
}

## accessing the value in a Dictionary using the key
my_dict['a']
# 1


## converting elements in a list using a Dictionary (Key: Value)
[my_dict[k] for k in my_list]
# [3, 1, 4, 2]

 

 

 

(3) 리스트에서 또 다른 리스트의 겹치는 원소를 빼기

 

리스트와 리스트 간 중복되는 원소 값 빼기는 TypeError: unsupported operand type(s) for -: 'list' and 'list' 에러를 반환합니다. 

 

## sample lists
a = [1, 2, 3, 4, 5]
b = [4, 5, 6, 7, 8]


## TypeError: unsupported operand type(s) for -: 'list' and 'list'
a - b
# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# <ipython-input-36-4dfa3698e4b8> in <module>
# ----> 1 a- b

# TypeError: unsupported operand type(s) for -: 'list' and 'list'

 

 

리스트 간 겹치는 값을 제거하려면 먼저 리스트를 Set 으로 변환을 해주고, 두 개의 Sets 간에 빼기를 해준 다음에, 집합 간 빼기 가 된 결과를 다시 list() 를 사용해서 리스트로 최종 변환해주면 됩니다. 

 

## substraction between lists using set
list(set(a) - set(b))
# [1, 2, 3]


list(set(b) - set(a))
# [8, 6, 7]

 

 

 

(4) 리스트 원소 정렬하기 (내림차순, 오름차순)

 

list.sort() 메소드를 사용해서 리스트 원소 (숫자형) 를 오름차순으로 정렬할 수 있습니다. 

내림차순 정렬을 하려면 list.sort(reverse=True) 처럼 옵션을 추가해주면 됩니다. 

## 리스트 원소 정렬
c = [8, 6, 7]
c.sort()
c
# [6, 7, 8]



## 리스트 원소 역순으로 정렬
c.sort(reverse=True)
c
# [8, 7, 6]

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

 

반응형
Posted by Rfriend

댓글을 달아 주세요

이번 포스팅에서는 Python의 Dictionary로 Key: Value 매핑하여 문자열을 변경해주는 replace() 함수를 사용하여 SQL query 코드의 여러개의 문자열을 변경하는 방법을 소개하겠습니다. 

코드가 길고 동일한 특정 변수나 테이블 이름이 여러번 나오는 경우, 수작업으로 일일이 하나씩 찾아가면서 변수나 테이블 이름을 변경하다보면 사람의 실수(human error)가 발생할 위험이 큽니다.  이럴 때 Dictionary에 (변경 전 : 변경 후) 매핑을 관리하고 컴퓨터에게 코드 변경을 시킬 수 있다면 사람의 실수를 예방하는데 도움이 될 것입니다. 

 

먼저, 예제로 사용할 SQL query와 Key(Before, 변경 전): Value(After, 변경 후) 를 매핑한 Dictionary 를 만들어보겠습니다. 

 

## key(Before): value(After) mapping dictionary
map_dict = {
    'X1': '"변수1"', # Key(Before): Value(After)
    'X2': '"변수2"', 
    'X3': '"변수3"',
    'MYTABLE': 'TEST_TABLE'
}


sql_query = "select \nx1, x2, x3 \nfrom mytable \nwhere x2 > 10 \nlimit 10;"

print(sql_query)
# select 
# x1, x2, x3 
# from mytable 
# where x2 > 10 
# limit 10;

 

 

 

아래에는 여러개의 줄(lines)을 가지는 문자열을 하나의 줄(line)로 나누어주는 splitlines() 메소드와, Dictionary의 Key, Value를 쌍으로 반환해주는 items() 메소드를 소개하였습니다. 

 

## splitlines()
for l in sql_query.splitlines():
    print(l)

# select 
# x1, x2, x3 
# from mytable 
# where x2 > 10 
# limit 10;



## replace(a, b) method convert a string 'a' with 'b'
s = 'Hello Python World.'
s.replace('Hello', 'Hi')

# 'Hi Python World.'



# dictionary.items() ==> returns key, value
for k, v in map_dict.items():
    print(k, ':', v)

# X1 : "변수1"
# X2 : "변수2"
# X3 : "변수3"
# MYTABLE : TEST_TABLE

 

 

 

위에서 기본적인 splitlines(), replace(), items() 메소드에 대한 사용법을 알았으니, 이제 이를 엮어서 코드에 있는 여러개의 특정 변수나 테이블 이름을 Dictionary(변경 전 이름 Key: 변경 후 이름 Value 매핑) 에서 가져와서 replace() 메소드로 변경해주는 사용자 정의함수를 만들어보겠습니다. 

 

## User Defined Function for converting codes 
## using replace() function and dictionary(Before: After mapping)
def code_converter(codes_old, map_dict):
    # blank string to save the converted codes
    codes_converted = ''
    
    # converting codes using replace() function and dictionary(Before: After mapping)
    for code in codes_old.splitlines():
        for before, after in map_dict.items():
            code = code.upper().replace(before, after)
            
        codes_converted = codes_converted + code + '\n'
        
    return codes_converted
    

## executing the UDF above
sql_query_new = code_converter(sql_query, map_dict)


print(sql_query_new)
# SELECT 
# "변수1", "변수2", "변수3" 
# FROM TEST_TABLE 
# WHERE "변수2" > 10 
# LIMIT 10;

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

지난번 포스팅에서는 독립된 2개 표본의 평균 차이를 검정하는 t-test 에 대해서 알아보았습니다. 
이번 포스팅에서는 Python을 이용한 짝을 이룬 표본의 평균 차이를 검정하는 paired t-test 에 대해서 소개하겠습니다. 
(R을 이용한 짝을 이룬 표본에 대한 평균 차이 검정은 https://rfriend.tistory.com/128 를 참고하세요)

짝을 이룬 t-test(paired t-test)는 짝을 이룬 측정치(paired measurements)의 평균 차이가 있는지 없는지를 검정하는 방법입니다. 데이터 값이 짝을 이루어서 측정되었다는 것에 대해 예를 들어보면, 사람들의 그룹에 대해 신약 효과를 알아보기 위해 (즉, 신약 투약 전과 후의 평균의 차이가 있는지) 신약 투약 전과 후(before-and-after)를 측정한 데이터를 생각해 볼 수 있습니다.  

 

 

[ One sample t-test vs. Independent samples t-test vs. Paired samples t-test ]

paired samples t-test, image source: https://datatab.net/tutorial/paired-t-test

 


짝을 이룬 t-test(paired t-test)는 종속 표본 t-test (the dependent samples t-test), 짝을 이룬 차이 t-test (the paired-difference t-test), 매칭된 짝 t-test (the matched pairs t-test), 반복측정 표본 t-test (the repeated-sample t-teset) 등의 이름으로도 알려져있습니다. 동일한 객체에 대해서 전과 후로 나누어서 반복 측정을 합니다.  

 

아래의 도표에는 짝을 이룬 두 표본의 대응 비교 데이터셋에 대한 모습입니다. (Xi, Yi) 가 동일한 대상에 대해서 before-after 로 반복 측정되어서, 각 동일 객체의 전-후의 차이 (즉, Di = Xi - Yi) 에 대해서 검정을 진행하게 됩니다. 

 

paired t-test



[ 가정사항 (Assumptions) ]

(1) 측정 대상이 독립적(independent)이어야 합니다. 하나의 객체에 대한 측정은 어떤 다른 객체의 측정에 영향을 끼치지 않아야 합니다.  
(2) 각 짝을 이룬 측정치는 동일한 객체로 부터 얻어야 합니다. 예를 들면, 신약의 효과를 알아보기 위해 투약 전-후(before-after) 를 측정할 때 동일한 환자에 대해서 측정해야 합니다. 
(3) 짝을 이뤄 측정된 전-후의 차이 값은 정규분포를 따라야한다는 정규성(normality) 가정이 있습니다. 만약 정규성 가정을 충족시키지 못하면 비모수 검정(nonparametric test) 방법을 사용해야 합니다. 

 


[ (예제) 신약 치료 효과 여부 검정 ]

 

새로운 당뇨병 치료제를 개발한 제약사의 예를 계속 들자면, 치료에 지대한 영향을 주는 외부요인을 통제하기 위해 10명의 당뇨병 환자를 선별하여 1달 동안 '위약(placebo)'을 투여한 기간의 혈당 (Xi)과 동일 환자에게 '신약(new medicine)'을 투여한 1달 기간 동안의 혈당 수치(Yi)를 측정하여 짝을 이루어 혈당 차이를 유의수준 5%에서 비교하는 방법이 짝을 이룬 표본에 대한 검정이 되겠습니다. (palacebo 와 신약 투여 순서는 무작위로 선정. 아래 예는 그냥 예시로 아무 숫자나 입력해본 것임. 혈당 수치 이런거 전 잘 몰라요. ^^;)

 

* 귀무가설 (Null Hypothesis, H0): 신약 투입 효과가 없다 (Mu1 = Mu2, ie. Difference=0)
* 대립가설 (Alternative Hypothesis, H1): 신약 투입 효과가 있다 (Mu1 > Mu2, ie. Difference > 0, right-sided test)

 

 

[ Python scipy 모듈을 이용한 paired t-test 실행 ]

 

scipy 모듈의 scipy.stats.ttest_rel 메소드를 사용해서 쌍을 이룬 t-test 를 실행합니다. "Calculate the t-test on TWO RELATED samples of scores, a and b." 라는 설명처럼 TWO RELATED samples 에서 rel 을 타서 메소드 이름을 지었습니다. (저라면 ttest_paired 라고 메소드 이름 지었을 듯요...) 

 

alternative='two-sided' 가 디폴트 설정인데요, 이번 예제에서는 'H1: 신약이 효과가 있다. (즉, 신약 먹기 전보다 신약 먹은 후에 혈당이 떨어진다)' 는 가설을 검정하기 위한 것이므로 alternative='greater' 로 설정을 해주었습니다. 

 

## -- Paired t-test

import numpy as np
from scipy import stats

## sample data-set (repeated measurements of Before vs. After for the same objects)
bef = np.array([51.4, 52.0, 45.5, 54.5, 52.3, 50.9, 52.7, 50.3, 53.8, 53.1])
aft = np.array([50.1, 51.5, 45.9, 53.1, 51.8, 50.3, 52.0, 49.9, 52.5, 53.0])


## paired t-test using Python scipy module
# H0: New medicine is not effective (i.e., no difference b/w before and after)
# H1: New medicine is effective (i.e., there is difference b/w before and after)
stat, p_val = stats.ttest_rel(bef, aft, alternative='greater')

print('statistic:', stat, '   p-value:', p_val)
# statistic: 3.550688262985491    p-value: 0.003104595950799298

 

분석 결과 p-value 가 0.003 으로서 유의수준 0.05 하에서 귀무가설 (H0: 신약은 효과가 없다. 즉, before와 after의 차이가 없다) 을 기각(reject)하고, 대립가설(H1: 신약은 효과가 있다. 즉, before 보다 after의 혈당 수치가 낮아졌다)을 채택(accept) 합니다. 




[ Reference ]
* The Paired t-test
  : https://www.jmp.com/en_nl/statistics-knowledge-portal/t-test/paired-t-test.html
* scipy.stats.ttest_rel 
  (Calculate the t-test on TWO RELATED samples of scores, a and b.)
  : https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ttest_rel.html

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

이번 포스팅에서는 Python을 사용해서 두 집단 간 평균이 같은지 아니면 다른지를 검정하는 t-test 를 해보겠습니다. 

 

연속형 확률분포인 t-분포 (Student's t-distribution) 에 대해서는 https://rfriend.tistory.com/110 를 참고하세요. 

R을 사용한 독립된 두 집단간 모평균 차이에 대한 검정은 https://rfriend.tistory.com/127 를 참고하세요. 

 

모집단의 평균과 분산에 대해서는 알지 못하는 경우가 많으므로, 보통은 모집단에서 무작위로 표본을 추출(random sampling)해서 모집단의 평균과 분산을 추정합니다. 표본의 크기가 작은 집단 간 평균의 차이가 있는지를 검정할 때 t-분포에 기반한 t-통계량(t-statistics)을 사용하여 검정을 합니다. 

 

t-검정은 대상 표본 집단이 1개인지 2개인지에 따라서 아래와 같이 구분할 수 있습니다. 

  * One-sample t-test : 모집단의 평균이 귀무가설의 특정 평균 값과 같은지를 검정

  * Two-sample t-test: 두 모집단의 평균이 같다는 귀무가설을 검정

 

One-sample t-test와 Two-sample t-test에서 사용하는 통계량에 대해서는 아래에 정리해보았습니다. 

 

 

여기서부터는 독립된 두 표본 간의 평균 차이에 대한 t-검정 (independent two-sample t-test) 에 대해서만 자세하게 소개하도록 하겠습니다. 

 

(1) Two-sample t-test 의 가설 (Hypothesis)

 

 - 귀무가설 (Null Hypothesis, H0): Mu1 = M2 (두 모집단의 평균이 같다)

 - 대립가설 (Alternative Hypothesis, H1)

    -. 양측검정 대립가설 (two-sided test H1): Mu1 ≠ Mu2 (두 모집단의 평균이 같지 않다)

    -. 우측검정 대립가설 (right-tailed test H1): Mu1 > M2 (모집단1의 평균이 모집단2의 평균보다 크다)

    -. 좌측검정 대립가설 (left-tailed test H1): M1 < M2 (모집단1의 평균이 모집단2의 평균보다 작다) 

 

t-test 를 통해 나온 p-value 가 유의수준보다 작으면 귀모가설을 기각하고 대립가설을 채택(즉, 두 모집단의 평균이 차이가 있다)하게 됩니다. 

 

 

 

(2) Two-sample t-test 의 가정사항 (Assumptions)

 

Two-sample t-test 의 결과가 유효하기 위해서는 아래의 가정사항을 충족시켜야 합니다. 

 

 (a) 한 표본의 관측치는 다른 표본의 관측치와 독립이다. (independent) 

 (b) 데이터는 정규분포를 따른다. (normally distributed)

 (c) 두 집단의 표본은 동일한 분산을 가진다. (the same variance).

       (--> 이 가설을 만족하지 못하면 Welch's t-test 를 실행합니다.)

 (d) 두 집단의 표본은 무작위 표본추출법을 사용합니다. (random sampling)

 

정규성 검정(normality test)을 위해서 Kolmogorov-Smirnov test, Shapiro-Wilk test, Anderson-Darling test 등을 사용합니다. 등분산성 검정(Equal-Variance test) 을 위해서 Bartlett test, Fligner test, Levene test 등을 사용합니다. 

 

 

 

(3) Python을 이용한 Two-sample t-test 실행 

 

(3-1) 샘플 데이터 생성

 

먼저 numpy 모듈을 사용해서 정규분포로 부터 각 관측치 30개를 가지는 표본을 3개 무작위 추출해보겠습니다. 이중 표본집단 2개는 평균과 분산이 동일한 정규분포로 부터 무작위 추출하였으며, 나머지 1개 집단은 평균이 다른 정규분포로 부터 무작위 추출하였습니다. 

 

## generating sample dataset
import numpy as np

np.random.seed(1004) # for reproducibility
x1 = np.random.normal(loc=0, scale=1, size=30) # the same mean
x2 = np.random.normal(loc=0, scale=1, size=30) # the same mean
x3 = np.random.normal(loc=4, scale=1, size=30) # different mean


x1
# array([ 0.59440307,  0.40260871, -0.80516223,  0.1151257 , -0.75306522,
#        -0.7841178 ,  1.46157577,  1.57607553, -0.17131776, -0.91448182,
#         0.86013945,  0.35880192,  1.72965706, -0.49764822,  1.7618699 ,
#         0.16901308, -1.08523701, -0.01065175,  1.11579838, -1.26497153,
#        -1.02072516, -0.71342119,  0.57412224, -0.45455422, -1.15656742,
#         1.29721355, -1.3833716 ,  0.3205909 , -0.59086187, -1.43420648])

x2
# array([ 0.60998011,  0.51266756,  1.9965168 ,  1.42945668,  1.82880165,
#        -1.40997132,  0.49433367,  0.9482873 , -0.35274099, -0.15359935,
#        -1.18356064, -0.75440273, -0.85981073,  1.14256322, -2.21331694,
#         0.90651805,  2.23629   ,  1.00743665,  1.30584548,  0.46669171,
#        -0.49206651, -0.08727244, -0.34919043, -1.11363541, -1.71982966,
#        -0.14033817,  0.90928317, -0.60012686,  1.03906073, -0.03332287])

x3
# array([2.96575604, 4.15929405, 4.33053582, 4.02563551, 3.90786096,
#        3.08148823, 4.3099129 , 2.75788362, 3.66886973, 2.35913334,
#        3.72460166, 3.94510997, 5.50604364, 2.62243844, 2.74438348,
#        4.16120867, 3.57878295, 4.2341905 , 2.79844805, 5.48131392,
#        4.29105321, 4.4022031 , 3.58533963, 5.00502917, 5.45376705,
#        3.92961847, 4.52897801, 1.62104705, 3.24945253, 5.10641762])


## Box plot
import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(10, 8))
sns.boxplot(data=[x1, x2, x3])
plt.xlabel("Group", fontsize=16)
plt.ylabel("Value", fontsize=16)
plt.xticks([0, 1, 2], ["x1", "x2", "x3"], fontsize=14)
plt.show()

 

(3-2) t-test 가설 충족 여부 검정

 

t-검정의 가정사항으로서 정규성 검정(normality test)과 등분산성 검정 (equal variance test) 을 Python의 scipy 모듈을 사용해서 수행해보겠습니다. 

 

* Kolmogorov-Smirnov Test 정규성 검정 

  - (귀무가설, H0): 집단의 데이터가 정규 분포를 따른다. 

  - (대립가설, H1): 집단의 데이터가 정규 분포를 따르지 않는다. 

 

아래에 x1, x2, x3 의 세 집단에 대한 K-S 정규성 검정 결과 p-value 가 모두 유의수준 0.05 보다 크므로 귀무가설을 채택하여 세 집단의 데이터가 정규 분포를 따른다고 볼 수 있습니다. 

 

## (1) Normality test using Kolmogorov-Smirnov Test
import scipy.stats as stats

t_stat_x1, p_val_x1 = stats.kstest(x1, 'norm', args=(x1.mean(), x1.var()**0.5))
t_stat_x2, p_val_x2 = stats.kstest(x2, 'norm', args=(x2.mean(), x2.var()**0.5))
t_stat_x3, p_val_x3 = stats.kstest(x3, 'norm', args=(x3.mean(), x3.var()**0.5))

print('[x1]  t-statistics:', t_stat_x1, '  p-value:', p_val_x1)
print('[x2]  t-statistics:', t_stat_x2, '  p-value:', p_val_x2)
print('[x3]  t-statistics:', t_stat_x3, '  p-value:', p_val_x3)

# [x1]  t-statistics: 0.13719205314969185   p-value: 0.577558008887932
# [x2]  t-statistics: 0.11086245840821829   p-value: 0.8156064477001308
# [x3]  t-statistics: 0.09056001868899977   p-value: 0.9477307432911599

 

 

다음으로 집단 x1과 x2, 집단 x1과 x3에 대한 등분산 가정 검정 결과, p-value 가 모두 유의수준 0.05 보다 크므로 두 집단 간 분산이 같다고 할 수 있습니다. (귀무가설 H0: 두 집단 간 분산이 같다.)

 

## (2) Equal variance test using Bartlett's tes
var_test_stat_x1x2, var_test_p_val_x1x2 = stats.bartlett(x1, x2)
var_test_stat_x1x3, var_test_p_val_x1x3 = stats.bartlett(x1, x3)

print('[x1 vs. x2]', 'statistic:', var_test_stat_x1x2, '  p-value:', var_test_p_val_x1x2)
print('[x1 vs. x3]', 'statistic:', var_test_stat_x1x3, '  p-value:', var_test_p_val_x1x3)

# [x1 vs. x2] statistic: 0.4546474955289549   p-value: 0.5001361557169177
# [x1 vs. x3] statistic: 0.029962346601998174   p-value: 0.8625756934286083

 

처음에 샘플 데이터를 생성할 때 정규분포로 부터 분산을 동일하게 했었으므로 예상한 결과대로 잘 나왔네요. 

 

 

 

(3-3) 독립된 두 표본에 대한 t-test 평균 동질성 여부 검정

 

이제 독립된 두 표본에 대해 t-test 를 실행해서 두 표본의 평균이 같은지 다른지 검정을 해보겠습니다. 

 

 - (귀무가설 H0) Mu1 = Mu2 (두 집단의 평균이 같다)

 - (대립가설 H1) Mu1 ≠ Mu2 (두 집단의 평균이 다르다) 

 

분산은 서로 같으므로 equal_var = True 라고 매개변수를 설정해주었습니다. 

그리고 양측검정(two-sided test) 을 할 것이므로 alternative='two-sided' 를 설정(default)해주면 됩니다. (왜그런지 자꾸 에러가 나서 일단 코멘트 부호 # 로 막아놨어요. scipy 버전 문제인거 같은데요... 흠... 'two-sided'가 default 설정이므로 # 로 막아놔도 문제는 없습니다.)

 

## (3) Identification test using Independent 2 sample t-test

## x1 vs. x2
import scipy.stats as stats
t_stat, p_val = stats.ttest_ind(x1, x2, 
                                #alternative='two-sided', #‘less’, ‘greater’
                                equal_var=True)

print('t-statistic:', t_stat, '   p-value:', p_val)
#t-statistic: -0.737991822907993    p-value: 0.46349499774375136
#==> equal mean


## x1 vs. x3
import scipy.stats as stats
t_stat, p_val = stats.ttest_ind(x1, x3, 
                                #alternative='two-sided', #‘less’, ‘greater’
                                equal_var=True)

print('t-statistic:', t_stat, '   p-value:', p_val)
#t-statistic: -15.34800563666855    p-value: 4.370531118607397e-22
#==> different mean

 

(3-1)에서 샘플 데이터를 만들 때 x1, x2 는 동일한 평균과 분산의 정규분포에서 무작위 추출을 하였으며, x3만 평균이 다른 정규분포에서 무작위 추출을 하였습니다. 

 

위의 (3-3) t-test 결과를 보면 x1, x2 간 t-test 에서는 p-value 가 0.46으로서 유의수준 0.05 하에서 귀무가설(H0)을 채택하여 두 집단 x1, x2 의 평균은 같다고 판단할 수 있습니다. 

 

x1, x3 집단 간 t-test 결과를 보면 p-value 가 4.37e-22 로서 유의수준 0.05 하에서 귀무가설(H0)을 기각(reject)하고 대립가설(H1)을 채택(accept)하여 x1, x3 의 평균이 다르다고 판단할 수 있습니다. 

 

 

[ Reference ]

* Wikipedia Student's t-test: https://en.wikipedia.org/wiki/Student%27s_t-test

* Python scipy.stats.ttest_ind 메소드
: https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.ttest_ind.html

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요

지난번 포스팅에서는 차원 축소란 무엇이고 왜 하는지(https://rfriend.tistory.com/736)와 투영(projection) 방법을 사용하는 주성분 분석(PCA)을 통한 차원 축소 (https://rfriend.tistory.com/751) 에 대해서 소개하였습니다. 

 

이번 포스팅에서는 manifold learning approche 에 해당하는 비지도학습, 비선형 차원축소 방법으로서 LLE (Locally Linear Embedding) 알고리즘을 설명하겠습니다. 

 

(1) 매니폴드 학습 (Manifold Learning)

(2) Locally Linear Embedding (LLE) 알고리즘

(3) sklearn 을 사용한 LLE 실습

 

 

(1) 매니폴드 학습 (Maniflod Learning)

 

먼저 매니폴드 학습(Manifold Learning)에 대해서 간략하게 살펴보고 넘어가겠습니다.

매니폴드란 각 점 근처의 유클리드 공간과 국부적으로 유사한 위상 공간을 말합니다. (A manifold is a topological space that locally resembles Eucludian space near each point.) [1]  d-차원의 매니폴드는 n-차원 공간 (이때 d < n) 의 부분으로서 d-차원의 초평면(d-dimensional hyperplane)을 국부적으로 닮았습니다. 아래의 3차원 공간의 Swiss Roll 에서 데이터의 구성 특성과 속성을 잘 간직한 2차원의 평면(plane)을 생각해볼 수 있는데요, Swiss Roll 을 마치 돌돌 말린 종이(매니폴드)를 펼쳐서 2차원 공간으로 표현해본 것이 오른쪽 그림입니다. 

manifold geometry: unroll the swiss roll

매니폴드 학습(Manifold Learning)이란 데이터에 존재하는 매니폴드를 모델링하여 차원을 축소하는 기법을 말합니다. 매니폴드 학습은 대부분 실세계의 고차원 데이터가 훨씬 저차원의 매니폴드에 가깝게 놓여있다는 매니폴드 가정(Manifold Assumption) 혹은 매니폴드 가설(Maniflod Hypothesis)에 기반하고 있습니다. [2] 

 

위의 비선형인 3차원 Swiss Roll 데이터를 선형 투영 기반의 주성분 분석(PCA, Principal Component Analysis)이나 다차원척도법(MDS, Multi-Dimensional Scaling) 로 2차원으로 차원을 축소하려고 하면 데이터 내 존재하는 매너폴드를 학습하지 못하고 데이터가 뭉개지고 겹쳐지게 됩니다. 

 

매니폴드 가설은 분류나 회귀모형과 같은 지도학습을 할 때 고차원의 데이터를 저차원의 매니폴드 공간으로 재표현했을 때 더 간단하고 성과가 좋을 것이라는 묵시적인 또 다른 가설과 종종 같이 가기도 합니다. 하지만 이는 항상 그런 것은 아니며, 데이터셋이 어떻게 구성되어 있느냐에 전적으로 의존합니다. 

 

 

 

(2) Locally Linear Embedding (LLE) 알고리즘

 

LLE 알고리즘은 "Nonlinear Dimensionality Reduction by Locally Linear Embedding (2000)" [3] 논문에서 주요 내용을 간추려서 소개하겠습니다. 

 

LLE 알고리즘은 주성분분석이나(PCA) 다차원척도법(MDS)와는 달리 광범위하게 흩어져있는 데이터 점들 간의 쌍을 이룬 거리를 추정할 필요가 없습니다. LLE는 국소적인 선형 적합으로 부터 전역적인 비선형 구조를 복원합니다 (LLE revocers global nonlinear structure from locally linear fits.). 

 

논문에서 소개한 LLE 3단계 절차(steps of locally linear embedding)는 꽤 간단합니다. 

    (1단계) 각 데이터 점의 이웃을 선택 (Select neighbors)

    (2단계) 이웃으로부터 선형적으로 가장 잘 재구성하는 가중치를 계산 (Reconstruct with linear weights)

    (3단계) 가중치를 사용해 저차원의 임베딩 좌표로 매핑 (Map to embedded coordinates)

 

Steps of locally linear embedding (source: Sam T. Roweis and Lawrence K. Saul)

 

1단계에서 각 데이터 점별로 이웃을 할당할 때는 데이터 점들 간의 거리를 계산하는데요, 가령 K 최근접이웃(K nearest neighbors) 기법을 사용할 수 있습니다. 

 

 

2단계에서 각 데이터 점들의 이웃들로부터 각 점을 가장 잘 재구성하는 선형 회귀계수(linear coefficients, linear weights)를 계산해서 국소적인 기하 특성을 간직한 매너폴드를 학습합니다.  아래는 재구성 에러를 측정하는 비용함수인데요, 원래의 데이터점과 이웃들로 부터 계산한 선형 모형으로 재구성한 값과의 거리를 제곱하여 모두 더한 값입니다. 

the cost function of reconstruction

위의 비용함수를 최소로 하는 가중치 벡터 Wij 를 계산하게 되는데요, 이때 2가지 제약조건(constraints)이 있습니다. 

  - (a) 각 데이터 점은 단지 그들의 이웃들로 부터만 재구성됩니다.

          (만약 Xj 가 Xi의 이웃에 속하지 않는 데이터점이면 가중치 Wij = 0 이 됩니다.)

  - (b) 가중치 행렬 행의 합은 1이 됩니다. (sum(Wij) = 1) 

 

위의 2가지 제약조건을 만족하면서 비용함수를 최소로 하는 가중치 Wij 를 구하면 특정 데이터 점에 대해 회전(rotations), 스케일 조정(recalings), 그리고 해당 데이터 점과 인접한 데이터 점의 변환(translations) 에 있어 대칭(symmetry)을 따릅니다. 이 대칭에 의해서 (특정 참조 틀에 의존하는 방법과는 달리) LLE의 재구성 가중치는 각 이웃 데이터들에 내재하는 고유한 기하학적 특성(저차원의 매너폴드)을 모델링할 수 있게됩니다. 

 

 

3단계에서는 고차원(D) 벡터의 각 데이터 점 Xi 를 위의 2단계에서 계산한 가중치를 사용하여 매니폴드 위에 전역적인 내부 좌표를 표현하는 저차원(d) 벡터 Yi 로 매핑합니다.  이것은 아래의 임베팅 비용 함수를 최소로 하는 저차원 좌표 Yi (d-mimensional coordinates) 를 선택하는 것으로 수행됩니다. 

the cost function of embedding

임베팅 비용 함수는 이전의 선형 가중치 비용함수와 마찬가지로 국소 선형 재구성 오차를 기반으로 합니다. 하지만 여기서 우리는 Yi 좌표를 최적화하는 동안 선형 가중치 Wij 를 고정합니다. 임베팅 비용 함수는 희소 N x N 고유값 문제(a sparse N X N eignevalue problem)를 풀어서 최소화할 수 있습니다. 이 선형대수 문제를 풀면 하단의 d차원의 0이 아닌 고객벡터는 원점을 중심으로 정렬된 직교 좌표 집합을 제공합니다. 

 

LLE 알고리즘은 희소 행렬 알고리듬(sparse matrix algorithms)을 활용하기 위해 구현될 때 비선형 차원축소 기법 중 하나인 Isomap 보다 더 빠른 최적화와 많은 문제에 대해 더 나은 결과를 얻을 수 있는 몇 가지 장점이 있습니다. [4]

 

아래에 3차원 Swiss Roll 데이터를 여러가지 비선형 차원 축소 기법을 사용해서 적용한 결과인데요 [5], LLE 는 수행 시간이 짧은 장점이 있지만 매니폴드가 약간 찌그러져 있는 한계가 있는 반면에, Isomap과 Hessian LLE 는 국지적인 데이터 형상과 관계를 잘 재표현한 저차원 매니폴드를 잘 잡아내지만 수행 시간이 LLE 대비 굉장히 오래걸리는 단점이 있습니다. 

 

 

 

(3) sklearn 을 사용한 LLE 실습

 

먼저 sklearn 모듈의 make_swiss_roll 메소드를 사용해서 데이터 점 1,000개를 가지는 3차원의 Swiss Roll 샘플 데이터셋을 만들어보겠습니다. X 는 데이터 점이고, t는 매니폴드 내 점의 주 차원에 따른 샘플의 단변량 위치입니다. 

 

## Swiss Roll sample dataset
## ref: https://scikit-learn.org/stable/modules/generated/sklearn.datasets.make_swiss_roll.html
from sklearn.datasets import make_swiss_roll

X, t = make_swiss_roll(n_samples=1000, noise=0.1, random_state=1004)


## X: The points.
X[:5]
# array([[ 1.8743272 , 18.2196214 , -4.75535504],
#        [12.43382272, 13.9545544 ,  2.91609936],
#        [ 8.02375359, 14.23271056, -8.67338106],
#        [12.23095692,  2.37167446,  3.64973091],
#        [-8.44058318, 15.47560926, -5.46533069]])


## t: The univariate position of the sample according to the main dimension of the points in the manifold.
t[:10]
# array([ 5.07949952, 12.78467229, 11.74798863, 12.85471755,  9.99011767,
#         5.47092408,  6.89550966,  6.99567358, 10.51333994, 10.43425738])

 

 

 

다음으로 sklearn 모듈에 있는 LocallyLinearEmbedding 메소드를 사용해서 위에서 생성한 Swiss Roll 데이터에 대해 LLE 알고리즘을 적용하여 2차원 데이터로 변환을 해보겠습니다. 

 

## Manifold Learning
from sklearn.manifold import LocallyLinearEmbedding

lle = LocallyLinearEmbedding(n_components=2, n_neighbors=10)

X_reduced_lle = lle.fit_transform(X)

X_reduced_lle[:10]
# array([[-0.0442308 ,  0.0634603 ],
#        [ 0.04241534,  0.01060574],
#        [ 0.02712308,  0.01121903],
#        [ 0.04396569, -0.01883799],
#        [ 0.00275144,  0.01550906],
#        [-0.04178513,  0.05415933],
#        [-0.03073913,  0.023496  ],
#        [-0.02880368, -0.0230327 ],
#        [ 0.0109238 , -0.02566617],
#        [ 0.00979253, -0.02309815]])

 

 

마지막으로 matplotlib 모듈을 사용해서 2차원 평면에 위에서 LLE 로 매핑한 데이터를 시각화해보겠습니다. 색깔을 t 로 구분하였습니다. 

 

## 2D Scatter Plot
import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (8, 8)
plt.scatter(X_reduced_lle[:, 0], X_reduced_lle[:, 1], c=t, cmap=plt.cm.hot)
plt.title("Unrolled Swiss Roll by LLE", fontsize=20)
plt.xlabel("$y_1$", fontsize=14)
plt.ylabel("$y_2$", fontsize=14)
plt.axis([-0.065, 0.055, -0.1, 0.12])
plt.grid(True)

plt.show()

 

 

 

[Reference]

[1] Wikipedia, "Manifold", https://en.wikipedia.org/wiki/Manifold

[2] Aurelien Geron, "Hands-On Machine Learning with Scikit-Learn & Tensorflow"

[3] Sam T. Roweis, Lawrence K. Saul, "Nonlinear Dimensionality Reduction by Locally Linear Embedding"

[4] Wikipedia, "Nonlinear Dimensionality Reduction",  https://en.wikipedia.org/wiki/Nonlinear_dimensionality_reduction

[5] Nik Melchior, "Manifold Learning, Isomap and LLE":  https://www.cs.cmu.edu/~efros/courses/AP06/presentations/melchior_isomap_demo.pdf 

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요!  :-)

반응형
Posted by Rfriend

댓글을 달아 주세요

지난 포스팅에서는 차원 축소란 무엇이고 왜 하는지, 무슨 방법이 있는지에 대해서 알아보았습니다.

(https://rfriend.tistory.com/736)  차원축소하는 방법에는 크게 Projection-based dimensionality reduction, Manifold Learning 의 두가지 방법이 있다고 했습니다. 

 

이번 포스팅에서는 투사를 통한 차원축소 방법(dimensionality reduction via projection approach) 으로서 주성분분석을 통한 차원축소(dimensionality reduction using PCA, Principal Component Analysis)에 대해서 소개하겠습니다. 

 

(1) 주성분분석(PCA, Principal Component Analysis)을 통한 차원 축소

(2) 특이값 분해 (SVD, Singular Value Decomposition)을 통한 차원 축소

 

 

 

(1) 주성분 분석(PCA, Principal Component Analysis)을 통한 차원 축소

 

주성분 분석(PCA)의 핵심 아이디어만 간략하게 소개하자면요, 피쳐 공간(Feature Space)에서 데이터의 분산을 최대로 잡아낼 수 있는 축을 제1 주성분 축으로 잡고, 이 제1 주성분 축과 직교(orthogonal)하는 축을 제2 주성분 축으로 잡고, ..., 이렇게 최대 변수의 개수 p 개 만큼 주성분 축을 잡아줍니다. (물론, 차원축소를 하는 목적이면 주성분 개수 m 이 변수 개수 p 보다는 작아야 겠지요). 그리고 축을 회전시켜주면 돼요. 

 

아래의 예시 도면을 보면 파란색 제 1 주성분 축 (1st principal component axis)이 데이터 분산을 가장 많이 설명하고 있는 것을 알 수 있습니다. 빨간색 점선의 제 2 주성분 축(2nd principal component axis) 은 제1 주성분 축과 직교하구요. 

 

 

Principal Component Analysis

 

이제 Python 을 가지고 실습을 해볼께요. 

(R로 주성분 분석 하는 것은 https://rfriend.tistory.com/61 를 참고하세요.)

 

먼저 예제로 사용할 iris 데이터셋을 가져오겠습니다. sepal_length, sepal_width, petal_length, petal_width 의 4개 변수를 가진 데이터셋인데요, 4개 변수 간 상관관계 분석을 해보니 상관계수가 0.8 이상으로 꽤 높게 나온 게 있네요. 주성분분석으로 차원축소 해보면 이쁘게 나올거 같아요. 

 

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

## loading IRIS dataset
from sklearn.datasets import load_iris
data = load_iris()


data['data'][:10]
# array([[5.1, 3.5, 1.4, 0.2],
#        [4.9, 3. , 1.4, 0.2],
#        [4.7, 3.2, 1.3, 0.2],
#        [4.6, 3.1, 1.5, 0.2],
#        [5. , 3.6, 1.4, 0.2],
#        [5.4, 3.9, 1.7, 0.4],
#        [4.6, 3.4, 1.4, 0.3],
#        [5. , 3.4, 1.5, 0.2],
#        [4.4, 2.9, 1.4, 0.2],
#        [4.9, 3.1, 1.5, 0.1]])


## converting into pandas DataFrame
iris_df = pd.DataFrame(
    data['data'], 
    columns=['sepal_length', 'sepal_width', 
             'petal_length', 'petal_width'])

iris_df.head()
# 	sepal_length	sepal_width	petal_length	petal_width
# 0	5.1	3.5	1.4	0.2
# 1	4.9	3.0	1.4	0.2
# 2	4.7	3.2	1.3	0.2
# 3	4.6	3.1	1.5	0.2
# 4	5.0	3.6	1.4	0.2


## correlation matrix
iris_df.corr()
# 	sepal_length	sepal_width	petal_length	petal_width
# sepal_length	1.000000	-0.117570	0.871754	0.817941
# sepal_width	-0.117570	1.000000	-0.428440	-0.366126
# petal_length	0.871754	-0.428440	1.000000	0.962865
# petal_width	0.817941	-0.366126	0.962865	1.000000

 

 

주성분 분석은 비지도 학습 (Unsupervised Learning) 이다보니 정답이라는게 없습니다. 그래서 분석가가 주성분의 개수를 지정해주어야 하는데요, 주성분의 개수가 적을 수록 차원 축소가 많이 되는 반면 정보 손실(information loss)가 발생하게 되며, 반면 주성분 개수가 많을 수록 정보 손실은 적겠지만 차원 축소하는 의미가 퇴색됩니다. 그래서 적절한 주성분 개수를 선택(hot to decide the number of principal components)하는게 중요한데요, 주성분의 개수별로 설명 가능한 분산의 비율 (percentage of explained variance by principal components) 을 많이 사용합니다. 

 

아래의 예에서는 첫번째 주성분이 분산의 92.4%를 설명하고, 두번째 주성분이 분산의 5.3%를 설명하므로, 주성분 1 & 2 까지 사용하면 전체 분산의 97.7%를 설명할 수 있게 됩니다. (즉, 원래 4개 변수를 2개의 차원으로 축소하더라도 분산의 97.7%를 설명 가능하다는 뜻) 

 

참고로, 만약 주성분분석 결과를 지도학습(가령, 회귀분석)의 설명변수 인풋으로 사용한다면, cross validation을 사용해서 주성분 개수별로 모델의 성능을 평가(가령, 회귀분석의 경우 MSE)해서, 모델 성능지표가 가장 좋은 주성분 개수를 선택하는 것도 좋은 방법입니다. 

 

## how to decide the number of Principal Components
from sklearn.decomposition import PCA

pca = PCA(random_state=1004)
pca.fit_transform(iris_df)


## percentage of variance explained
print(pca.explained_variance_ratio_)
# [0.92461872 0.05306648 0.01710261 0.00521218]


## Principal 1 & 2 explain about 97.8% of variance
plt.rcParams['figure.figsize'] = (7, 7)
plt.plot(range(1, iris_df.shape[1]+1), pca.explained_variance_ratio_)
plt.xlabel("number of Principal Components", fontsize=12)
plt.ylabel("% of Variance Explained", fontsize=12)
plt.show()

Explained Variance by Principal Components

 

 

이제 주성분 개수를 2개로 지정(n_components=2)해서 주성분 분석을 실행해보겠습니다. Python의 sklearn 모듈의 decomposition.PCA 메소드를 사용하겠습니다. 

 

## Dimensionality Reduction with n_components=2
pca = PCA(n_components=2, random_state=1004)
iris_pca = pca.fit_transform(iris_df)


iris_pca[:10]
# array([[-2.68412563,  0.31939725],
#        [-2.71414169, -0.17700123],
#        [-2.88899057, -0.14494943],
#        [-2.74534286, -0.31829898],
#        [-2.72871654,  0.32675451],
#        [-2.28085963,  0.74133045],
#        [-2.82053775, -0.08946138],
#        [-2.62614497,  0.16338496],
#        [-2.88638273, -0.57831175],
#        [-2.6727558 , -0.11377425]])

 

 

 

위에서 실행한 주성분분석 결과를 가지고 시각화를 해보겠습니다. 4개 변수를 2개의 차원으로 축소를 했기 때문에 2차원의 산점도로 시각화를 할 수 있습니다. 이때 iris 데이터셋의 target 속성정보를 이용해서 붓꽃의 품종별로 색깔과 모양을 달리해서 산점도로 시각화해보겠습니다. 

 

## Visualization

## target
data['target'][:5]
# array([0, 0, 0, 0, 0])


## mapping target name using numpy vectorization
species_map_dict = {
    0: 'setosa', 
    1: 'versicolor', 
    2: 'virginica'
}

iris_pca_df = pd.DataFrame({
    'pc_1': iris_pca[:, 0], 
    'pc_2': iris_pca[:, 1], 
    'species': np.vectorize(species_map_dict.get)(data['target']) # numpy broadcasting
})


iris_pca_df.head()
# pc_1	pc_2	species
# 0	-2.684126	0.319397	setosa
# 1	-2.714142	-0.177001	setosa
# 2	-2.888991	-0.144949	setosa
# 3	-2.745343	-0.318299	setosa
# 4	-2.728717	0.326755	setosa


import seaborn as sns
import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (7, 7)
sns.scatterplot(
    x='pc_1', 
    y='pc_2',
    hue='species', 
    style='species',
    s=100,
    data=iris_pca_df
)

plt.title('PCA result of IRIS dataset')
plt.xlabel('Principal Component 1', fontsize=14)
plt.ylabel('Principal Component 2', fontsize=14)
plt.show()

PCA result of iris dataset

 

 

 

(2) 특이값 분해 (SVD, Singular Value Decomposition)을 통한 차원 축소

 

선형대수의 특이값 분해의 결과로 나오는 U, sigma, V 에서 V 가 주성분 분석의 주성분에 해당합니다.  

특이값 분해(SVD, Singular Value Decomposition)에 대한 이론적인 소개는 https://rfriend.tistory.com/185 를 참고하세요. 

 

numpy 모듈의 linalg.svd 메소드를 사용하여 특이값 분해를 하려고 할 때 먼저 데이터 표준화(standardization)을 수작업으로 진행해 줍니다. (sklearn 으로 주성분분석을 할 때 sklearn 모듈이 내부적으로 알아서 표준화해서 진행해줌). 

 

## Standardization first
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(data['data'])


## PCA assumes that the dataset is centered around the origin.
X_centered = data['data'] - data['data'].mean(axis=0)
X_centered[:5]
# array([[-0.74333333,  0.44266667, -2.358     , -0.99933333],
#        [-0.94333333, -0.05733333, -2.358     , -0.99933333],
#        [-1.14333333,  0.14266667, -2.458     , -0.99933333],
#        [-1.24333333,  0.04266667, -2.258     , -0.99933333],
#        [-0.84333333,  0.54266667, -2.358     , -0.99933333]])

 

 

웨에서 표준화한 데이터를 numpy 모듈의 linalg.svd 메소드를 사용하여 특이값 분해를 해준 후에, V 를 transpose (T) 해주어서 첫번째와 두번째 열의 값을 가져오면 제1 주성분, 제2 주성분을 얻을 수 있습니다. 

 

## standard matrix factorization using SVD
U, s, V = np.linalg.svd(X_scaled.T)


## V contains all the principal components
pc_1 = V.T[:, 0]
pc_2 = V.T[:, 1]


## check pc_1, pc_2
pc_1[:10]
# array([0.10823953, 0.09945776, 0.1129963 , 0.1098971 , 0.11422046,
#        0.099203  , 0.11681027, 0.10671702, 0.11158214, 0.10439809])


pc_2[:10]
# array([-0.0409958 ,  0.05757315,  0.02920003,  0.05101939, -0.0552418 ,
#        -0.12718049, -0.00406897, -0.01905755,  0.09525253,  0.04005525])

 

 

 

위에서 특이값분해(SVD)로 구한 제1 주성분, 제2 주성분을 가지고 산점도를 그려보겠습니다. 이때 iris 의 target 별로 색깔과 모양을 달리해서 시각화를 해보겠습니다. 

 

## Visualization

iris_svd_df = pd.DataFrame({
    'pc_1': pc_1, 
    'pc_2': pc_2, 
    'species': np.vectorize(species_map_dict.get)(data['target']) # numpy broadcasting
})


import seaborn as sns
import matplotlib.pyplot as plt

plt.rcParams['figure.figsize'] = (7, 7)
sns.scatterplot(
    x='pc_1', 
    y='pc_2',
    hue='species', 
    style='species',
    s=100,
    data=iris_svd_df
)

plt.title('SVD result of IRIS dataset')
plt.xlabel('Principal Component 1', fontsize=14)
plt.ylabel('Principal Component 2', fontsize=14)
plt.show()

dimensionality reduction by SVD

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요. 

 

반응형
Posted by Rfriend

댓글을 달아 주세요

이번 포스팅에서는 Python 을 이용해서 의사결정나무 (Decision Tree) 를 시각화하는 3가지 방법을 소개하겠습니다. 

 

(1) sklearn.tree.export_text 메소드를 이용해서 의사결정나무를 텍스트로 인쇄하기 (text representation)

(2) sklearn.tree.plot_tree 메소드와 matplotlib 을 이용해서 의사결정나무 시각화

(3) sklearn.tree.export_graphviz 메소드와 graphviz 를 이용해서 의사결정나무 시각화

 

 

먼저, 예제로 사용할 iris 데이터셋과 필요한 모듈을 불어오겠습니다. 

 

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt 

df = pd.DataFrame({
    'x1': [1, 2, 2, 2, 2, 3, 3, 4, 5, 5], 
    'x2': [2, 2, 3, 4, 5, 4, 6, 3, 2, 5], 
    'cat': [0, 1, 1, 0, 0, 1, 0, 1, 1, 0]
})


df

# x1	x2	cat
# 0	1	2	0
# 1	2	2	1
# 2	2	3	1
# 3	2	4	0
# 4	2	5	0
# 5	3	4	1
# 6	3	6	0
# 7	4	3	1
# 8	5	2	1
# 9	5	5	0


## scatter plot
import seaborn as sns

plt.figure(figsize=(7, 7))
sns.scatterplot(x='x1', y='x2', hue='cat', style='cat', s=100, data=df)
plt.show()

scatter plot by group

 

 

 

(0) sklearn.tree 메소드를 이용해서 의사결정나무(Decision Tree) 모델 훈련하기

 

from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import cross_val_score
from sklearn import tree

## getting X, y values
X = df[['x1', 'x2']]
y = df['cat']

## initiating DecisionTreeClassifer method
dt_clf = DecisionTreeClassifier(random_state = 1004)


## fitting a decision tree classifier
dt_clf_model = dt_clf.fit(X, y)


## feature importances
dt_clf_model.feature_importances_

#array([0.43809524, 0.56190476])

 

 

 

(1) sklearn.tree.export_text 메소드를 이용해서 의사결정나무를 텍스트로 인쇄하기 (text representation)

 

의사결정나무 모델을 적합한 후에 사용자 인터페이스가 없는 애플리케이션에서 로그(log) 텍스트로 모델 적합 결과를 확인할 때 사용할 수 있습니다. 또는 의사결정나무의 적합된 룰을 SQL 로 구현하여 운영 모드로 적용하고자 할 때 아래처럼 텍스트로 인쇄를 하면 유용하게 사용할 수 있습니다. 

 

## Text Representation
dt_clf_model_text = tree.export_text(dt_clf_model)

print(dt_clf_model_text)

|--- feature_1 <= 4.50
|   |--- feature_0 <= 1.50
|   |   |--- class: 0
|   |--- feature_0 >  1.50
|   |   |--- feature_1 <= 3.50
|   |   |   |--- class: 1
|   |   |--- feature_1 >  3.50
|   |   |   |--- feature_0 <= 2.50
|   |   |   |   |--- class: 0
|   |   |   |--- feature_0 >  2.50
|   |   |   |   |--- class: 1
|--- feature_1 >  4.50
|   |--- class: 0

 

 

 

(2) sklearn.tree.plot_tree 메소드와 matplotlib 을 이용해서 의사결정나무 시각화

 

(3)번 처럼 Graphviz 를 설치하지 않아도, matplotlib 을 사용해서 간편하게 의사결정나무를 시각화 할 때 유용합니다. 편리한 대신에 (3)번 Graphviz 대비 자유도가 떨어지는 단점이 있습니다. 

 

## Plot Tree with plot_tree
fig = plt.figure(figsize=(15, 8))
_ = tree.plot_tree(dt_clf_model, 
                  feature_names=['x1', 'x2'],
                  class_names=['0', '1'],
                  filled=True)

 

 

 

(3) sklearn.tree.export_graphviz 메소드와 graphviz 를 이용해서 의사결정나무 시각화

 

Graphviz 의 시각화 기능을 fully 사용할 수 있으므로 매우 강력한 시각화 방법이라고 할 수 있습니다. 

Graphviz 설치하고 Decision Tree 시각화하기는 https://rfriend.tistory.com/382 를 참고하세요. 

DOT language 에 대해서는 https://graphviz.org/doc/info/lang.html 를 참고하세요. 

 

## Visualizing Tree using Graphviz
from sklearn import tree
import graphviz

## exporting tree in DOT format
## refer to: https://scikit-learn.org/stable/modules/generated/sklearn.tree.export_graphviz.html
tree_dot = tree.export_graphviz(
    dt_clf_model, 
    feature_names=['x1', 'x2'], 
    class_names=['0', '1'],
    filled=True
)


## draw graph using Graphviz
dt_graph = graphviz.Source(tree_dot, format='png')
dt_graph

 

decition tree visualization using graphviz

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요~!

 

 

반응형
Posted by Rfriend

댓글을 달아 주세요

이번 포스팅에서는 Python을 사용해서 

 

(1) 텍스트 데이터 전처리 (text data pre-processing)

(2) 토큰화 (tokenization)

 

하는 방법을 소개하겠습니다. 

 

 

(1) 텍스트 데이터 전처리 (text data pre-processing)

 

텍스트 데이터 전처리를 하는데는 (a) Python의 텍스트 처리 내장 메소드 (Python built-in methods)와 (b) 정규 표현식 매칭 연산(regular expression matching operations)을 제공하는 Python re 패키지를 사용하겠습니다. re 패키지는 Python을 설치할 때 디폴트로 같이 설치가 되므로 별도로 설치할 필요는 없습니다. 

 

예제로 사용할 Input 텍스트는 인터넷쇼핑몰의 고객별 거래내역에 있는 구매 품목 텍스트 데이터이며, Output 은 텍스트 전처리 후의 고객별 구매 품목의 리스트입니다. 

 

예) Input: '**[세일]** 말티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!!...외5건'

예) Output: [말티즈, 강아지사료, 습식, 소프트]

 

[ 텍스트 데이터 전처리 절차 ]

    (1-1) [], (), {}, <> 괄호와 괄호 안 문자 제거하기

    (1-2) '...외', '...총' 제거하기

    (1-3) 특수문자, 숫자 제거

    (1-4) 단위 제거: cm, km, etc.

    (1-5) 공백 기준으로 분할하기

    (1-6) 글자 1개만 있으면 제외하기

    (1-7) 텍스트 데이터 전처리 사용자 정의함수(User Defined Function) 정의 

    (1-8) pandas DataFrame의 텍스트 칼럼에 데이터 전처리 사용자 정의함수 적용

 

 

(1-1) [], (), {}, <> 괄호와 괄호 안 문자 제거하기

 

Python의 정규 표현식(regular expression)을 다루는 re 패키지를 사용해서 다양한 형태의 괄호와 괄호안의 문자를 매칭한 후에 '' 로 대체함으로써 결과적으로 제거하였습니다. re.sub() 는 pattern 과 매치되는 문자열을 repl 의 문자열로 대체를 시켜줍니다. 

 

정규표현식에서 문자 클래스를 만드는 메타 문자인 [ ] 로 만들어지는 정규표현식은 [ ]사이에 들어가는 문자 중 어느 한개라도 매치가 되면 매치를 시켜줍니다. 가령, [abc] 의 경우 'a', 'b', 'c' 중에서 어느 하나의 문자라도 매치가 되면 매치가 되는 것으로 간주합니다. 

 

## Python Regular expression operations
import re

## sample text
s = '**[세일]** 말티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!!...외5건'


## (1-1) [], (), {}, <> 괄호와 괄호 안 문자 제거하기
pattern = r'\([^)]*\)'  # ()
s = re.sub(pattern=pattern, repl='', string=s)

pattern = r'\[[^)]*\]'  # []
s = re.sub(pattern=pattern, repl='', string=s)

pattern = r'\<[^)]*\>'  # <>
s = re.sub(pattern=pattern, repl='', string=s)

pattern = r'\{[^)]*\}'  # {}
s = re.sub(pattern=pattern, repl='', string=s)

print(s)
[Out] 
# **** 말티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!!...외5건

 

 

 

(1-2) '...외', '...총' 제거하기

 

Python의 내장 문자열 메소드인 replace() 를 사용해서 '...외', '...총' 을 ' ' 로 대체함으로써 제거하였습니다. 

 

## (1-2) '...외', '...총' 제거하기
s = s.replace('...외', ' ')
s = s.replace('...총', ' ')

print(s)
[Out]
# **** 말티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!! 5건

 

 

 

(1-3) 특수문자, 숫자 제거

 

정규표현식에서 하이픈(-)은 from ~ to 의 범위를 나타냅니다. [a-zA-Z] 는 소문자와 대문자 영어 모두를 의미하며, [가-힣] 은 한글 전체를 의미합니다. 

 

정규표현식에서 [^] 는 not 의 의미이며, 아래의 [^a-zA-Z가-힣] 은 앞에 '^' 가 붙었으므로 영어와 한글이 아닌(not, ^) 문자, 즉 특수문자와 숫자와 매칭이 됩니다. 

 

## (1-3) 특수문자, 숫자 제거
pattern = r'[^a-zA-Z가-힣]'
s = re.sub(pattern=pattern, repl=' ', string=s)

print(s)
[Out] 
# 말티즈 강아지사료 습식 소프트 신  종   Kg       원    건

 

 

 

(1-4) 단위 제거: cm, km, etc.

 

## (1-4) 단위 제거: cm, km, etc.
units = ['mm', 'cm', 'km', 'ml', 'kg', 'g']
for unit in units:
    s = s.lower() # 대문자를 소문자로 변환
    s = s.replace(unit, '')
    
print(s)
[Out] 
# 말티즈 강아지사료 습식 소프트 신  종          원    건

 

 

 

(1-5) 공백 기준으로 분할하기

 

Python 내장형 문자열 메소드인 split() 을 사용해서 공백(space)을 기준으로 문자열을 분할하였습니다. 

 

## (1-5) 공백 기준으로 분할하기
s_split = s.split()

print(s_split)
[Out] 
# ['말티즈', '강아지사료', '습식', '소프트', '신', '종', '원', '건']

 

 

 

(1-6) 글자 1개만 있으면 제외하기

 

글자 길이가 1 보다 큰 (len(word) != 1) 글자만 s_list 의 리스트에 계속 추가(append) 하였습니다. 

 

## (1-6) 글자 1개만 있으면 제외하기
s_list = []
for word in s_split:
    if len(word) !=1:
        s_list.append(word)
        
print(s_list)
[Out] 
# ['말티즈', '강아지사료', '습식', '소프트']

 

 

 

(1-7) 텍스트 데이터 전처리 사용자 정의함수(User Defined Function) 정의 

 

위의 (1-1) ~ (1-6) 까지의 텍스트 전처리 과정을 아래에 사용자 정의함수로 정의하였습니다. 문자열 s 를 input으로 받아서 텍스트 전처리 후에 s_list 의 단어들을 분할해서 모아놓은 리스트를 반환합니다. 

 

## 텍스트 전처리 사용자 정의함수(UDF of text pre-processing)
def text_preprocessor(s):
    import re
    
    ## (1) [], (), {}, <> 괄호와 괄호 안 문자 제거하기
    pattern = r'\([^)]*\)'  # ()
    s = re.sub(pattern=pattern, repl='', string=s)
    
    pattern = r'\[[^)]*\]'  # []
    s = re.sub(pattern=pattern, repl='', string=s)
    
    pattern = r'\<[^)]*\>'  # <>
    s = re.sub(pattern=pattern, repl='', string=s)
    
    pattern = r'\{[^)]*\}'  # {}
    s = re.sub(pattern=pattern, repl='', string=s)
    
    ## (2) '...외', '...총' 제거하기
    s = s.replace('...외', ' ')
    s = s.replace('...총', ' ')
    
    ## (3) 특수문자 제거
    pattern = r'[^a-zA-Z가-힣]'
    s = re.sub(pattern=pattern, repl=' ', string=s)
    
    ## (4) 단위 제거: cm, km, etc.
    units = ['mm', 'cm', 'km', 'ml', 'kg', 'g']
    for unit in units:
        s = s.lower() # 대문자를 소문자로 변환
        s = s.replace(unit, '')
        
    # (5) 공백 기준으로 분할하기
    s_split = s.split()
    
    # (6) 글자 1개만 있으면 제외하기
    s_list = []
    for word in s_split:
        if len(word) !=1:
            s_list.append(word)
            
    return s_list
    

## sample text
s = '**[세일]** 말티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!!...외5건'

## apply the UDF above
s_list = text_preprocessor(s)
print(s_list)
[Out] 
# ['말티즈', '강아지사료', '습식', '소프트']

 

 

 

(1-8) pandas DataFrame의 텍스트 칼럼에 데이터 전처리 사용자정의함수 적용

 

pandas DataFrame에 위의 (1-7) 텍스트 전처리 사용자 정의함수를 적용하기 위해서는 apply() 와 lambda function 을 사용합니다. 

 

## pandas DataFrame
import pandas as pd

s1 = '**[세일] 몰티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!!...외5건'
s2 = '[시크루즈] 50%+추가20%/여름신상 루즈핏 롱원피스/상하세트/점프슈트...외3건'
s3 = '올챌린지 KF94 마스크 100매 국내생산 여름용 황사 화이트...총2건'
s4 = '[최대혜택가] ##하림 용가리치킨 300gX3봉 외 닭가슴살/튀김 골라담기...외12건'
s5 = '[20%+15%] 종아리알 타파! 무로 요가링/마사지릴/압박스타킹/마사지볼...외4종'

df = pd.DataFrame({
    'id': [1, 2, 3, 4, 5], 
    'items': [s1, s2, s3, s4, s5]
})

print(df)
[Out]
#    id                                              items
# 0   1  **[세일] 몰티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!!...외5건
# 1   2     [시크루즈] 50%+추가20%/여름신상 루즈핏 롱원피스/상하세트/점프슈트...외3건
# 2   3           올챌린지 KF94 마스크 100매 국내생산 여름용 황사 화이트...총2건
# 3   4   [최대혜택가] ##하림 용가리치킨 300gX3봉 외 닭가슴살/튀김 골라담기...외12건
# 4   5    [20%+15%] 종아리알 타파! 무로 요가링/마사지릴/압박스타킹/마사지볼...외4종


## Apply the text preprocessing UDF using apply() and lambda function
df['items_list'] = df['items'].apply(lambda s: text_preprocessor(s))


print(df['items'])
print('-------------'*5)
print(df['items_list'])
[Out]
# 0    **[세일] 몰티즈 강아지사료 습식 소프트 신 3종 15Kg 39,000원!!...외5건
# 1       [시크루즈] 50%+추가20%/여름신상 루즈핏 롱원피스/상하세트/점프슈트...외3건
# 2             올챌린지 KF94 마스크 100매 국내생산 여름용 황사 화이트...총2건
# 3     [최대혜택가] ##하림 용가리치킨 300gX3봉 외 닭가슴살/튀김 골라담기...외12건
# 4      [20%+15%] 종아리알 타파! 무로 요가링/마사지릴/압박스타킹/마사지볼...외4종
# Name: items, dtype: object
# -----------------------------------------------------------------
# 0                     [몰티즈, 강아지사료, 습식, 소프트]
# 1         [추가, 여름신상, 루즈핏, 롱원피스, 상하세트, 점프슈트]
# 2       [올챌린지, kf, 마스크, 국내생산, 여름용, 황사, 화이트]
# 3               [하림, 용가리치킨, 닭가슴살, 튀김, 골라담기]
# 4    [종아리알, 타파, 무로, 요가링, 마사지릴, 압박스타킹, 마사지볼]
# Name: items_list, dtype: object

 

 

 

위에 Jupyter Notebook 에서 pandas DataFrame을 출력한 결과가 중앙 정렬로 되어있어서 보기가 불편한데요, 아래처럼 좌측 정렬 (left alignment) 을 해서 보기에 편하도록 해보았습니다. 

 

## align text of pandas DataFrame to left in Jupyter Notebook
dfStyler = df.style.set_properties(**{'text-align': 'left'})
dfStyler.set_table_styles([dict(selector='th', props=[('text-align', 'left')])])

text preprocessing using regular expressions

 

 

 

(2) 토큰화 (tokenization)

 

토큰화(Tokenization)는 말뭉치(Corpus)를 토큰이라고 불리는 단어 또는 문장으로 나누는 것을 말합니다. 이러한 토큰은 문맥(Context)을 이해하거나 NLP에 대한 모델을 개발하는 데 사용됩니다. 

 

POS 태킹 (Part-of-Speech Tagging) 은 널리 사용되는 자연어 처리 프로세스로, 단어의 정의와 문맥에 따라 언어의 특정 부분에 대응하여 텍스트(corpus)의 단어를 분류하는 것을 말합니다.

 

아래 코드는 위 (1)번의 텍스트 전처리에 이어서, 띄어쓰기가 제대로 되지 않아서 붙어 있는 단어들을, Python KoNLpy 패키지를 사용해서 형태소 분석의 명사를 기준으로 단어 토근화를 한 것입니다. ((2)번 words_tokonizer() UDF 안에 (1)번 text_preprocessor() UDF가 포함되어 있으며, 순차적으로 수행됩니다.)

 

KoNLpy 패키지는 Python으로 한국어 자연어 처리(NLP) 을 할 수 있게 해주는 패키지입니다. 그리고 Kkma 는 서울대학교의 IDS 랩에서 JAVA로 개발한 형태소 분석기(morphological analyzer)입니다.  

 

## insatll konlpy if it is not istalled yet
# ! pip install konlpy


## KoNLpy : NLP of the Korean language
## reference ==> https://konlpy.org/en/latest/
## Kkma is a morphological analyzer 
## and natural language processing system written in Java, 
## developed by the Intelligent Data Systems (IDS) Laboratory at SNU.
from konlpy.tag import Kkma


## define words tokenizer UDF
def words_tokonizer(text):
    from konlpy.tag import Kkma # NLP of the Korean language
    kkma = Kkma()
    
    words = []
    
    # Text preprocessing using the UDF above
    s_list = text_preprocessor(text)
    
    # POS tagging
    for s in s_list:
        words_ = kkma.pos(s)   
        
        # NNG indexing
        for word in words_:
            if word[1] == 'NNG':
                words.append(word[0])
            
    return words
    
    
## apply the UDF above as an example
words_tokonizer('강아지사료')
[Out] ['강아지', '사료']


words_tokonizer('상하세트')
[Out] ['상하', '세트']

 

 

위의 (2) words_tokenizer() UDF를 pandas DataFrame에 적용하기 위해서 apply() 함수와 lambda function 을 사용하면 됩니다. 

 

## apply the text tokenization UDF to pandas DataFrame using apply() and lambda function
df['items'].apply(lambda text: words_tokonizer(text))

[Out]
# 0 [몰티즈, 강아지, 사료, 습식, 소프트]
# 1 [추가, 여름, 신상, 루즈, 핏, 원피스, 상하, 세트, 점프, 슈트]
# 2 [챌린지, 마스크, 국내, 생산, 여름, 황사, 화이트]
# 3 [하림, 용가리, 치킨, 닭, 가슴살, 튀김]
# 4 [종아리, 타파, 무로, 요가, 링, 마사지, 압박, 스타, 킹, 마사지]
# Name: items, dtype: object

 

 

이번 포스팅이 많은 도움이 되었기를 바랍니다. 

행복한 데이터 과학자 되세요! :-)

 

반응형
Posted by Rfriend

댓글을 달아 주세요