행렬의 값이 대부분 '0'인 행렬을 희소행렬(Sparse matrix) 이라고 하며, 반대로 행렬의 값이 대부분 '0이 아닌 값'을 가지는 경우 밀집행렬(Dense matrix) 혹은 조밀행렬이라고 합니다. 


가령, 자연어처리(NLP)에서 텍스트를 파싱해서 TF-IDF 행렬을 만들다보면 대부분의 값은 '0'으로 채워져 있고 '0'이 아닌 값은 듬성듬성 들어있는 희소행렬을 주로 얻게 됩니다. 


희소행렬(Sparse matrix)의 경우 대부분의 값이 '0'이므로 이를 그대로 사용할 경우 메모리 낭비가 심하고 또 연산시간도 오래 걸리는 단점이 있습니다. 이런 단점을 피하기 위해 희소행렬을 다른 형태의 자료구조로 변환해서 저장하고 사용합니다. 


희소행렬을 저장하는 자료구조 4가지에는 


(a) Dictionary of keys(DOK): key (행번호, 열번호) 대 value (데이터) 매핑

(b) List of lists (LIL): 링크드 리스트 알고리즘을 이용한 저장 기법, 내용의 추가와 삭제가 용이하지만 CSR 대비 메모리 낭비가 큼

(c) Coordinate list (COO): (행, 열, 값) 튜플 목록 저장

(d) Compressed sparse row (CSR): 가로의 순서대로 재정렬하는 방법으로 행에 관여하여 정리 압축


가 있습니다. 

* reference: https://en.wikipedia.org/wiki/Sparse_matrix



이중에서 Compressed Sparse Row(CSR) 형태(format) 자료구조의 장점과 단점을 표로 정리해보면 아래와 같습니다. 


 CSR 자료구조의 장점

(Advantages of the CSR format)

CSR 자료구조의 단점

(Disadvantages of the CSR format)

  •  효율적인 산술 연산
     (예: CSR + CSR, CSR * CSR 등)
  • 효율적인 행 슬라이싱
    (efficient row slicing)
  • 빠른 행렬 벡터 곱
    (fast matrix vector products)
  • 느린 열 슬라이싱
    (slow column slicing) 
    --> Compressed Sparse Column format 고려
  • 희소성 구조의 변화 시 연산비용 큼
    --> LIL, DOK 고려



이번 포스팅에서는 희소행렬에 대해 이들 중에서도 SciPy 모듈의 csr_matrix() 메소드를 사용하여 


(1) NumPy 희소행렬을 SciPy 압축 희소 행(CSR) 행렬로 변환하기

   (Converting a NumPy sparse matrix to a SciPy compressed sparse row matrix)


(2) SciPy 압축 희소 행(CSR) 행렬을 NumPy 희소행렬로 변환하기 

   (Converting a SciPy compressed sparse row matrix to a NumPy sparse matrix)


하는 각 2가지 방법을 소개하겠습니다. 






  (1) NumPy array 행렬을 SciPy 압축 희소 행(CSR) 행렬로 변환하기

      (Transforming a NumPy matrix to a SciPy compressed sparse row matrix)


NumPy array 데이터형의 희소행렬을 SciPy 압축 희소 행(CSR) 행렬 (compressed sparse row matrix)로 만드는 3가지 방법을 아래의 arr 넘파이 배열을 예로 들어서 설명해보겠습니다. 


class scipy.sparse.csr_matrix(arg1, shape=None, dtype=None, copy=False)


(1-1) arr 넘파이 배열에 대해 => scipy.sparse.csr_matrix(arr)

(1-2) 값 data, '0'이 아닌 원소의 열 위치 indices, 행 위치 시작 indptr 

        => csr_matrix((data, indices, indptr), shape=(5, 4))

(1-3) 값 data, '0'이 아닌 원소의 (행, 열) 위치 => csr_matrix((data, (row, col)), shape=(5, 4))




Compressed Sparse Row matrix로 변환할 대상이 되는 NumPy array 예제 행렬인 'arr' 을 먼저 만들어보겠습니다. 



import numpy as np

from scipy.sparse import csr_matrix


arr = np.array([[0, 1, 0, 2], 

                [0, 3, 4, 5], 

                [0, 0, 0, 0], 

                [6, 0, 0, 7], 

                [0, 8, 0, 0]])


arr

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]])



(1-1) arr 넘파이 배열에 대해 => scipy.sparse.csr_matrix(arr)


NumPy 배열 (rank-2 ndarray), 희소행렬, 밀집행렬을 scipy.sparse.csr)matrix() 메소드 안에 넣어주면 되니 제일 쉬운 방법입니다. 



# converting NumPy array into SciPy Compressed Sparse Row matrix

csr_mat = csr_matrix(arr)


csr_mat

[Out] <5x4 sparse matrix of type '<class 'numpy.longlong'>'
	with 8 stored elements in Compressed Sparse Row format>

 



위에서 만든 'csr_mat' 이름의 5x4 sparse matrix (CSR format) 에서 특성값(attributes)으로서 

  - (a) csr_mat.indptr : 행렬의 '0'이 아닌 원소의 행의 시작 위치

  - (b) csr_mat.indices : 행렬의 '0'이 아닌 원소의 열의 위치

  - (c) csr_mat.data : 행렬의 '0'이 아닌 원소 값



print('-- Compressed Sparse Row matrix --')

print('indptr:', csr_mat.indptr)

print('indices:', csr_mat.indices)

print('data:', csr_mat.data)


-- Compressed Sparse Row matrix --
indptr: [0 2 5 5 7 8]
indices: [1 3 1 2 3 0 3 1]
data: [1 2 3 4 5 6 7 8]

 



이를 그림으로 좀더 알기 쉽게 표현을 해보면 아래와 같습니다. 헷갈리지 않고 좀더 알아보기에 편리하도록 NumPy array 행렬의 값(data)을 숫자가 아니라 영어 알파벳으로 바꾸어서 표시하였습니다. 



SciPy Compressed Sparse Row matrix 에서 

  - data 는 행렬의 '0'이 아닌 원소 값이므로 이해하기 어려운게 없습니다. 

  - indices 도 행렬의 '0'이 아닌 원소의 위치 (row, column) 에서 열(column) 위치(index) 배열 [1, 3, 1, 2, 3, 0, 3, 1 ] 이므로 어려울게 없습니다. 

  - indptr 은 저는 처음에 봤을 때는 이게 뭔가하고 유심히 보면서 좀 고민을 했습니다. ^^;  indptr은 행을 기준으로 했을 때 행별로 '0'이 아닌 원소가 처음 시작하는 위치의 배열입니다. 말로 설명하기 좀 어려운데요, 가령 위의 NumPy 배열 'arr'의 '0'이 아닌 원소의 위치 (행 row, 열 col) 배열(위 그림의 중간에 표시되어 있음)을 보면, 

'arr' 배열의 첫번째 행 [0, a, 0, b] 는 '0'이 아닌 원소의 (row, col) 배열0 위치에서 시작, 

               두번째 행 [0, c, d, e] 는 '0'이 아닌 원소의 (row, col) 배열의 2 위치에서 시작, 

               세번째 행 [0, 0, 0, 0] 는 '0'이 아닌 원소의 (row, col) 배열의 5 위치에서 시작, (비어있음) 

               네번째 행 [f, 0, 0, g] 는 '0'이 아닌 원소의 (row, col) 배열의 5 위치에서 시작, 

                        (--> 왜냐하면, 세번째 행의 모든 값이 '0' 이므로 같은 위치인 5에서 시작함)

               다섯번째 행 [0, h, 0, 0] 는 '0'이 아닌 원소의 (row, col) 배열의 7 위치에서 시작, 

               마지막으로, 'arr' 의 원소의 개수 8 에서 끝남.  


이렇게 indptr을 이용하는 이유는 행 기준의 '0'이 아닌 원소의 (row, col) 을 사용하는 것보다 데이터를 좀더 압축할 수 (즉, 줄일 수) 있기 때문입니다. 위의 예의 경우 row 기준으로 '0'이 아닌 원소의 (row, col)에서 row만 보면 [0, 0, 1, 1, 1, 3, 3, 4] 로서 [0, 0], [1, 1, 1], [3, 3] 처럼 같은 행에 두 개 이상의 '0'이 아닌 원소가 있으면 같은 행 숫자가 반복됩니다. 이럴 때 indptr 을 사용하면 [0, 2, 5, 5, 7, 8] 처럼 행 기준으로 '0'이 아닌 원소가 시작되는 row 위치만 가져오면 되므로 저장해야하는 정보량을 줄일 수 (압축) 있게 됩니다.   



(1-2) 값 data, '0'이 아닌 원소의 열 위치 indices, 행 위치 시작 indptr 

        => csr_matrix((data, indices, indptr), shape=(5, 4))


NumPy array 행렬이 없더라도, data, indices, indptr 입력값과 output 행렬의 형상(shape) 을 알고 있다면 SciPy Compressed Sparse Row matrix를 아래처럼 만들 수 있습니다.

(다만, indptr, indices 를 사람이 직접 입력하기에는 좀 어려운 면이 있어서 위의 (1-1) 방법보다는 좀 어려워보이네요.)



# converting NumPy array into SciPy Compressed Sparse Row matrix

indptr = np.array([0, 2, 5, 5, 7, 8]) # the location of the first element of the row.

indices = np.array([1, 3, 1, 2, 3, 0, 3, 1]) # column indices

data = np.array([1, 2, 3, 4, 5, 6, 7, 8])    # corresponding value


csr_mat2 = csr_matrix((data, indices, indptr), shape=(5, 4))

csr_mat2

[Out] <5x4 sparse matrix of type '<class 'numpy.int64'>'
	with 8 stored elements in Compressed Sparse Row format>



print('-- Compressed Sparse Row matrix 2 --')

print('indptr:', csr_mat2.indptr)

print('indices:', csr_mat2.indices)

print('data:', csr_mat2.data)


-- Compressed Sparse Row matrix 2 --
indptr: [0 2 5 5 7 8]
indices: [1 3 1 2 3 0 3 1]
data: [1 2 3 4 5 6 7 8]

 




(1-3) 값 data, '0'이 아닌 원소의 (행, 열) => csr_matrix((data, (row, col)), shape=(5, 4))


세번째는 행렬에서 '0' 이 아닌 원소의 값(data)과 (행, 열) 위치 (row_ind, col_ind), 그리고 행렬의 형상(shape) 을 입력해주는 방식입니다. (사람 입장에서는 이 (1-3) 방식이 위의 (1-2) 방식보다는 직관적으로 이해하기가 더 쉽기는 합니다.)



# converting NumPy array into SciPy Compressed Sparse Row matrix

row = np.array([0, 0, 1, 1, 1, 3, 3, 4])

col = np.array([1, 3, 1, 2, 3, 0, 3, 1])

data = np.array([1, 2, 3, 4, 5, 6, 7, 8])


csr_mat3 = csr_matrix((data, (row, col)), shape=(5, 4))

csr_mat3

[Out] <5x4 sparse matrix of type '<class 'numpy.longlong'>'
	with 8 stored elements in Compressed Sparse Row format>

 

print('-- Compressed Sparse Row matrix 3 --')

print('indptr:', csr_mat3.indptr)

print('indices:', csr_mat3.indices)

print('data:', csr_mat3.data)


-- Compressed Sparse Row matrix 2 --
indptr: [0 2 4 4 6 7]
indices: [1 3 1 2 0 3 1]
data: [1 2 3 4 5 6 7]


-- Compressed Sparse Row matrix 3 --
indptr: [0 2 5 5 7 8]
indices: [1 3 1 2 3 0 3 1]
data: [1 2 3 4 5 6 7 8]





  (2) SciPy 압축 희소 행(CSR) 행렬을 NumPy 행렬로 변환하기

     (Transforming a SciPy compressed sparse row matrix into a NumPy matrix) 


SciPy 압축 희소 행 행렬을 NumPy 행렬로 변환하기는 아래 2가지 메소드를 이용하면 매우 쉽습니다. 


(2-1) scipy.sparse.csr_matrix.toarray() 메소드

(2-2) scipy.sparse.csr_matrix.todense() 메소드



위에서 만든 'csr_mat', 'csr_mat2', 'csr_mat3' 세 개의 압축 희소 행(CSR) 행렬을 아래에서 원래의 NumPy array 배열로 변환해보니 모두 동일하게 제대로 변환이 되었네요. 


(2-1) scipy.sparse.csr_matrix.toarray() 메소드



# converting sparse matrix to NumPy array

csr_mat.toarray()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)


csr_mat2.toarray()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)


csr_mat3.toarray()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)





(2-2) scipy.sparse.csr_matrix.todense() 메소드


SciPy Compressed Sparse Row matrix를 원래의 행렬로 변환할 때 그게 희소행렬(Sparse matrix) 일 수도 있고 아니면 밀집행렬(Dense matrix) 일 수도 있기 때문에 메소드 이름을 csr_matrix.todense() 라고 하면 좀 오해의 소지도 있어서 썩 잘 지은 메소드 이름은 아니라고 생각하는데요, 어쨌든 반환된 후의 결과는 위의 csr_matrix.toarray() 와 동일합니다. 



csr_mat.todense()

[Out]
array([[0, 1, 0, 2],
       [0, 3, 4, 5],
       [0, 0, 0, 0],
       [6, 0, 0, 7],
       [0, 8, 0, 0]], dtype=int64)

 




  (3) 동일 위치에 중복된 원소값은 합산 (Duplicate entries are summed together.)


아래의 행렬처럼 (row, column) 이 (0, 0)인 위치에 5, 3 의 값이 중복되어 있고, (1, 1)인 위치에 2, 4 의 값이 중복되어 있는 Compressed Sparse Row matrix 데이터는 중복된 위치의 값을 더해주게 됩니다. 


  5 + 3

 0

 0

 0

 2 + 4

 0

 0

 0

 0



# Duplicate entries are summed together. 

row = np.array([0, 1, 1, 0])

col = np.array([0, 1, 1, 0])

data = np.array([5, 2, 4, 3])

csr_matrix((data, (row, col)), shape=(3, 3)).toarray()


[Out]
array([[8, 0, 0],
       [0, 6, 0],
       [0, 0, 0]], dtype=int64)

 



[ Reference ]

* SciPy 모듈 sparse.csr_matrix() 메소드

  : https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.csr_matrix.html

* Sparse matrix: https://en.wikipedia.org/wiki/Sparse_matrix



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 값 배열에 가중치 배열을 곱해서 합한 가중합(weighted sum)을 구하는 3가지 방법을 소개하겠습니다. 


a 를 가중치, b 를 값 배열이라고 했을 때, 


(1) 내적을 이용한 가중합 계산: np.dot(a, b) or np.matmul(a, b)

(2) 브로드캐스팅(broadcasting)을 이용하여 가중치와 값을 원소끼리 곱한 후 합하는

     np.sum(a.reshape(5, 1) * b, axis=0)

(3) repeat()로 가중치를 값 배열 1축만큼 반복 생성한 후, 가중치와 값의 원소끼리 곱한 후 합하는, 

     np.sum(a.reshape(5, 1).repeat(3, axis=1) * b, axis=0)




먼저, numpy를 import하고, 예제로 사용할 가중치 배열 a와, 값의 행렬 b를 만들어보겠습니다. 



import numpy as np


# weights

a = np.array([0.5, 0.3, 0.1, 0.08, 0.02])


print('a shape:', a.shape)

a shape: (5,)


print(a)

[0.5  0.3  0.1  0.08 0.02]



# values

b = np.arange(15).reshape(5, 3)


print('b shape:', b.shape)

b shape: (5, 3)


print(b)

[[ 0  1  2]

 [ 3  4  5]

 [ 6  7  8]

 [ 9 10 11]

 [12 13 14]]

 




  (1) 내적을 이용한 가중합 계산: np.dot(a, b) 또는 np.matmul(a, b)


가장 편리한 방법은 np.dot() 또는 np.matmul() 메소드를 사용하여 내적(inner prodct, dot product)을 계산하는 것입니다. 이때 가중치 벡터 a 에 대해서는 형태 변환(reshape)을 할 필요가 없이 그대로 사용할 수 있습니다.  



np.dot(a, b)

Out[2]: array([2.46, 3.46, 4.46])


np.matmul(a, b)

Out[3]: array([2.46, 3.46, 4.46])

 




  (2) Broadcasting을 이용하여 가중치와 값을 원소끼리 곱한 후, axis=0으로 합하기


이번에는 위의 (1) 내적을 계산의 각 단계별로 분리해서 순서대로 해보겠습니다. 가중치 a와 값 b의 원소끼리 곱한 후에, axis=0을 기준으로 합할 것입니다. 


먼저, 가중치 a와 값 b를 원소끼리 곱하기 위해 가중치 a의 형태(shape)를 기존의 (5,)에서 a.reshape(5, 1) 을 적용하여 (5, 1) 의 형태로 변환을 해줍니다. 값이 들어있는 배열 b의 형태는 (5, 3) 이므로 가중치 배열 a의 (5, 1) 형태를 값 배열 b에 곱해주면 ==> 서로 형태가 같지 않으므로 numpy 는 가중치 a 배열 (5, 1) 을 (5, 3)으로 자동으로 형태 변환을 시켜서 값 배열 b 의 (5, 3) 형태와 동일하게 맞추어 주어 원소간 곱을 해줍니다. 이러한 기능을 브로드캐스팅(boradcasting) 이라고 합니다. 



# shape of a_rs and b are different

a_rs = a.reshape(5, 1)

print(a_rs.shape)

print(a_rs)

(5, 1)


print(b.shape)

(5, 3)


# multiply using boradcasting of a_rs

a_rs_b_mult = a_rs * b


print(a_rs_b_mult.shape)

(5, 3)


print(a_rs_b_mult)

[[0.   0.5  1.  ]

 [0.9  1.2  1.5 ]

 [0.6  0.7  0.8 ]

 [0.72 0.8  0.88]

 [0.24 0.26 0.28]]



# weighted sum

np.sum(a_rs_b_mult, axis=0)

Out[9]: array([2.46, 3.46, 4.46])



* numpy 배열들의 다른 차원의 배열 간 산술연산 시 Broadcasting 은 아래 포스팅을 참고하세요. 

https://rfriend.tistory.com/287




  (3) repeat()로 가중치를 반복 생성한 후, 가중치와 값을 원소끼리 곱한 후 합하기


위의 (2)번에서는 가중치 배열 a의 형태를 바꾼 후의 a_rs 배열과 값 b 배열을 곱할 때, 사람 눈에는 보이지않게 numpy가 알아서 자동으로 가중치 a_rs 배열 (5, 1) 형태를 브로드캐스팅(broadcasting)을 해주어서 (5, 3) 형태로 만들어서 원소끼리 곱해주었습니다. 




반면에, 이번 (3)번에서는 사람이 repeat(n, axis) 메소드를 사용해서 명시적으로 배열을 n번 만큼 axis 축을 기준으로 반복해주어서 (2)번의 브로드캐스팅의 역할을 수행해주는 것입니다. 


구현 관점에서 보면 브로드케스팅이 편리한 장점이 있고, 반면에 repeat() 메소드로 명시적으로 기입을 해주면 코딩하는 사람이 이해하기 쉬운 장점이 있습니다. 



# match the shape of a and b by repeatition 

a_rs_rp = a.reshape(5, 1).repeat(3, axis=1)


print(a_rs_rp.shape)

(5, 3)


print(a_rs_rp)

[[0.5  0.5  0.5 ]

 [0.3  0.3  0.3 ]

 [0.1  0.1  0.1 ]

 [0.08 0.08 0.08]

 [0.02 0.02 0.02]]



# multiplication of a_rs_rp and b per each elements

a_rs_rp_b_mult = a_rs_rp * b


print(a_rs_rp_b_mult.shape)

(5, 3)


print(a_rs_rp_b_mult)

[[0.   0.5  1.  ]

 [0.9  1.2  1.5 ]

 [0.6  0.7  0.8 ]

 [0.72 0.8  0.88]

 [0.24 0.26 0.28]]



# weighted sum

np.sum(a_rs_rp_b_mult, axis=0)

Out[17]: array([2.46, 3.46, 4.46])

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 Python numpy 모듈의 random.choice() 메소드를 사용하여 임의(무작위, 확률) 추출 (random sampling)하는 방법을 소개하겠습니다. 


numpy.random.choice() 메소드의 기본 Syntax는 아래와 같습니다. 각 parameter별로 예를 들어서 설명을 해보겠습니다. 


numpy.random.choice(a, size=None, replace=True, p=None)


이때 표본을 추출할 모집단에 해당하는 a 는 1-D array-like 또는 np.arange(n) 의 정수가 됩니다. 





  (1) 1부터 5까지의 정수 모집단에서 3개의 균등확률표본을 복원추출 하기

  (Generate a uniform random sample from np.arange(5) of size 3)


표본이 뽑힐 확률 p를 명시적으로 지정해주지 않으면 모든 원소가 뽑힐 확률이 동일한 (즉, p=1/N) 균등확률분포를 가정하고 표본이 추출됩니다. 

그리고, 복원추출(replacement)이 기본 설정값이므로 똑같은 값이 2번 이상 표본으로 뽑힐 수도 있습니다. 



import numpy as np


np.random.choice(5, 3) # with replacement

Out[3]: array([4, 0, 0])

 


참고로, 위의 np.random.choice(5, 3) 코드는 np.random.randint(0,5,3) 과 동일합니다. 



균등확률분포로 부터 임의 추출이므로 매번 표본으로 뽑히는 값이 바뀌게 됩니다. 위와 코드는 같지만 추출된 표본은 다르지요?



np.random.choice(5, 3) # sampled with different values

Out[4]: array([2, 0, 0])

 



복원추출을 하게 되면 1~5 의 정수를 가지는 모집단에서 5개를 초과하는 표본을 뽑는데 문제가 없습니다. 



np.random.choice(5, 10) # with replacement

Out[5]: array([2, 2, 4, 2, 4, 0, 0, 4, 3, 2])

 




 (2) 1~5의 정수 모집단에서 비균등 확률 p 로 3개 원소 임의 표본 복원추출 하기

    (Generate a non-uniform random sample from np.arange(5) of size 3:)


만약 모집단 내 각 원소별로 표본으로 뽑힐 확률 p를 알고 있다면, 혹은 명시적으로 지정을 해주고 싶다면 모수 p에 표본이 추출될 확률을 입력해주면 됩니다. 



p = [0.1, 0, 0.3, 0.6, 0]

np.random.choice(5, 3, p=p)

Out[7]: array([2, 3, 3])

 



만약 표본을 추출할 모집단(a)의 원소 개수(n)과 각 원소별 표본이 뽑힐 확률(p)의 원소 개수가 서로 다를 경우 ValueError 가 발생합니다. (아래 예에서는 확률 p에는 3개가, 모집단 a는 5개 원소로서 서로 다름)



# ValueError: 'a' and 'p' must have same size

p = [0.1, 0, 0.3]           # size 3

np.random.choice(5, 3, p=p) # size 5


ValueError: 'a' and 'p' must have same size

 



만약 모집단 원소별 표본으로 뽑힐 확률 p의 전체 합이 1 이 아니거나 0~1사이 값이 아닌 경우도 ValueError가 발생합니다. 왜냐하면, 확률의 정의 상 (1) 각 사건별 확률의 전체 합은 1이고, (2) 각 사건별 확률은 0~1 사이의 실수를 가져야 하기 때문입니다. 



# ValueError: probabilities do not sum to 1

p = [0.4, 0, 0.3, 0.6, 0]   # sum to 1.3 (not 1)

np.random.choice(5, 3, p=p)

ValueError: probabilities do not sum to 1



# ValueError: probabilities are not non-negative

p = [-0.4, 0, 0.3, 1.6, 0]   

np.random.choice(5, 3, p=p)


ValueError: probabilities are not non-negative





  (3) 1~5 정수 모집단에서 3개의 균등확률표본을 비복원추출(non-replacement) 하기

  (Generate a uniform random sample from np.arange(5) of size 3 

   without replacement)


옵션 중에서 replace=False 로 설정을 해주면 비복원추출(without replacement)을 합니다. 즉, 모집단에서 표본을 추출할 때 각 원소를 딱 한번만 추출하기 때문에 동일한 원소가 2번 이상 뽑히는 일은 없습니다. (default 옵션은 replace=True 임)



# Generate a uniform random sample from np.arange(5) of size 3 without replacement:

np.random.choice(5, 3, replace=False)

Out[11]: array([3, 2, 1])

 


참고로 위의 np.random.choice(5, 3, p=p) 코드는 np.random.permutation(np.arange(5))[:3] 과 동일합니다. 



비복원추출을 할 때는 한가지 조심해야 할 것이 있는데요, 모집단의 원소 개수보다 많은 수의 샘플을 비복원추출(replace=False)하려고 하면 ValueError가 발생합니다. (아래 예처럼, 5개의 원소를 가진 모집단에서 10개 표본을 비복원(즉, 중복 없이) 추출할 수는 없겠지요!)



# ValueError: Cannot take a larger sample than population when 'replace=False'

np.random.choice(5, 10, replace=False)


ValueError: Cannot take a larger sample than population when 'replace=False'

 



위의 (2)번에서 표본추출확률 p 를 설정하는 것과, 이번 (3)번의 비복원추출(replace=False)을 함께 설정해줌으로써 비균등확률(non-uniform random sample)로 비복원추출(without replacement) 샘플링도 물론 가능합니다. 



# Generate a non-uniform random sample from np.arange(5) of size 3 without replacement:

p = [0.1, 0, 0.3, 0.6, 0]

np.random.choice(5, 3, replace=False, p=p)

Out[13]: array([3, 0, 2])

 




  (4) 정수 대신에 임의의 배열처럼 생긴 객체의 원소를 확률추출하기

  (Any of the above can be repeated with an arbitrary array-like instead of just integers.)


np.random.choice(a, size=None, replace=True, p=None) 의 syntax에서 a 부분에 정수 배열 (np.arange(n)) 말고도 1-D array-like 객체를 대상으로 할 수도 있습니다. 아래 예에서는 과일 이름을 원소로 가지는 리스트로부터 비복원 비균등 확률 표본추출을 해보았습니다. 



# Any of the above can be repeated with an arbitrary array-like instead of just integers.

fruits = ['apple', 'banana', 'cherries', 'durian', 'grapes', 'lemon', 'mango']

p = [0.1, 0, 0.2, 0.5, 0.1, 0.05, 0.05]

np.random.choice(fruits, 3, p=p, replace=False)

Out[14]: array(['cherries', 'lemon', 'durian'], dtype='<U8')

 




  (5) 초기값 설정을 통한 재현가능성 확보

  (setting seed number for reproducibility)


np.random.seed() 로 초기값(seed value)을 설정해주면 매번 똑같은 확률표본을 추출할 수 있습니다. 만약 재현가능성(reproducibility)이 필요한 경우라면 초기값을 설정해주세요. 



# set seed number for reproducibility

np.random.seed(1004)

np.random.choice(5, 3)

Out[15]: array([2, 3, 3])


np.random.seed(1004)

np.random.choice(5, 3)

Out[16]: array([2, 3, 3])




참고로, 기계학습을 할 때 train set, test set 을 무작위로 분할하는 여러가지 방법은 https://rfriend.tistory.com/519 를 참고하시기 바랍니다. 


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)


* Reference: https://docs.scipy.org/doc//numpy-1.10.4/reference/generated/numpy.random.choice.html



Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 numpy의 meshgrid()와 matplotlib의 contourf(xx, yy, Z) 함수를 사용하여 Random Forest Classifer 의 의사결정 경계(Decision Boundary)를 시각화해보겠습니다.  


(1) iris dataset 의 species 를 분류할 수 있는 Random Forest 분류 모델 훈련

(2) numpy의 meshgrid() 로 설명변수 공간의 좌표들을 생성하여 Random Forest 분류 모델로 예측

(3) matplotlib의 contourf() 함수로 Random Forest Classifier 예측 결과 시각화(채우기)

    : 의사결정 경계(Decision Boundary)

(4) iris dataset 의 species 의 산점도를 겹쳐서 그리기


의 순서로 진행해보겠습니다. 


  (1) iris dataset 의 species 를 분류할 수 있는 Random Forest 분류 모델 훈련


먼저 iris dataset 을 업로드하고 어떻게 생긴 데이터인지 살펴보겠습니다. 



#%% import libraries

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

import seaborn as sns


#%% iris dataset

iris = sns.load_dataset('iris')

iris.info()

<class 'pandas.core.frame.DataFrame'>

RangeIndex: 150 entries, 0 to 149

Data columns (total 5 columns):

 #   Column        Non-Null Count  Dtype  

---  ------        --------------  -----  

 0   sepal_length  150 non-null    float64

 1   sepal_width   150 non-null    float64

 2   petal_length  150 non-null    float64

 3   petal_width   150 non-null    float64

 4   species       150 non-null    object 

dtypes: float64(4), object(1)

memory usage: 6.0+ KB



iris.groupby('species').size()

Out[1]: 

species

setosa        50

versicolor    50

virginica     50

dtype: int64



#%% scatter plot by 'species'

sns.scatterplot(x='petal_length', 

                y='petal_width', 

                hue='species', 

                style='species', 

                s=100, 

                data=iris)

plt.show()



이번 포스팅의 시각화 예에서는 'sepal_length', 'sepal_width' 의 두개 칼럼을 설명변수로 사용하고, 'species'를 목표변수로 하여 붓꽂 종류(species)를 Random Forest Classifier 로 분류하는 모델을 만들어보겠습니다. 


(* 이번 포스팅의 주 목적은 모델의 의사결정경계 시각화이므로, 모델링 단계에서의 train/test set 분할, 모델 성과 평가 등은 생략합니다.)



#%% Classification using Random Forest

from sklearn.ensemble import RandomForestClassifier


X = np.array(iris[['petal_length', 'petal_width']])

y = iris['species']

y = np.where(y=='setosa', 0, np.where(y=='versicolor', 1, 2))


rfc = RandomForestClassifier(max_depth=2, n_estimators=200, random_state=1004)

rfc.fit(X, y)

 



  (2) numpy의 meshgrid() 로 설명변수 공간의 좌표들을 생성하여 

      Random Forest 분류 모델로 예측


위의 (1)번에서 적합된 Random Forest Classifier 모델을 사용해서 붓꽂 종류를 예측하고자 할때는 predict() 라는 메소드를 사용하면 됩니다. 



#%% predict

pred = rfc.predict(X)

print(pred == y)

[ True  True  True  True  True  True  True  True  True  True  True  True

  True  True  True  True  True  True  True  True  True  True  True  True

  True  True  True  True  True  True  True  True  True  True  True  True

  True  True  True  True  True  True  True  True  True  True  True  True

  True  True  True  True  True  True  True  True  True  True  True  True

  True  True  True  True  True  True  True  True  True  True False  True

  True  True  True  True  True False  True  True  True  True  True False

  True  True  True  True  True  True  True  True  True  True  True  True

  True  True  True  True  True  True  True  True  True  True False  True

  True  True  True  True  True  True  True  True  True  True  True False

  True  True  True  True  True  True  True  True  True  True  True  True

  True False False  True  True  True  True  True  True  True  True  True

  True  True  True  True  True  True]

 



(a) 이제 x 축('petal_length')과 y 축('petal_width')의 최소값, 최대값을 구하고, (b) 이들 x축과 y축의 최소값 ~ 최대값 사이의 구간을 h = 0.01 단위의 간격으로 좌표를 생성하여, (c) np.meshgrid()로 이들 x축과 y축의 h=0.01 단위의 좌표 벡터를 가지고 격자 행렬을 생성합니다. 

(d) 이렇게 생성한 격자 행렬 좌표값들에 대해 Random Forest Classifier 훈련된 모델의 predict() 메소드로 예측을 하여 contour plot의 높이에 해당하는 Z 값을 구합니다. 



# create coordinate matrices from x_min~x_max, y_min~y_max coordinates

h = 0.01

x_min, x_max = X[:, 0].min() - .1, X[:, 0].max() + .1

y_min, y_max = X[:, 1].min() - .1, X[:, 1].max() + .1

xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))

XY = np.c_[xx.ravel(), yy.ravel()]


XY.shape

Out[4]: (158600, 2)


# predict

pred_cls = rfc.predict(XY)


# align the shape of Z with xx

Z = pred_cls.reshape(xx.shape)

 




  (3) matplotlib.contourf() 로 Random Forest Classifier 예측 결과 시각화

      -->  (4) iris dataset 의 species 의 산점도를 겹쳐서 그리기


위의 (2)번에서 만들어놓은 x, y 격자 행렬 좌표값별로 Random Forest Classifier 예측 결과를 matplotlib의 confourf() 함수로 시각화 (채우기)를 하면 아래와 같습니다. 아래에 색깔로 구분된 영역들과 경계선이 바로 Random Forest Classifier가 훈련 데이터로 부터 학습한 species 분류 의사결정 경계(Decision Boundary)가 되겠습니다. 



# Random Forest Classifier: Decision Boundary

plt.contourf(xx, yy, Z)

plt.axis('off')




좀더 보기에 쉽도록 위의 의사결정경계 그래프 위에다가 학습에 사용했던 iris species 값들을 species별로 marker를 구분하여 겹쳐서 다시 한번 시각화해보겠습니다. 



plt.contourf(xx, yy, Z)

plt.axis('off')


sns.scatterplot(x='petal_length', 

                y='petal_width', 

                hue='species', 

                style='species', 

                s=100, 

                data=iris)

plt.title('Decision Boundary by Random Forest', fontsize=14)

plt.show()



 



iris species 별 산점도를 의사결정경계 배경 위에 겹쳐 그릴 때 seaborn.scatterplot() 대신에 아래의 코드처럼 matplotlib.scatter() 과 for loop을 사용해서 그릴 수도 있습니다. 



plt.contourf(xx, yy, Z)

plt.axis('off')


# or plot data points using matplotlib and for loop

N = 50

CLS_NUM = 3

markers = ['o', 'x', 's']

for i in range(CLS_NUM):

    plt.scatter(X[i*N: (i+1)*N, 0], X[i*N:(i+1)*N, 1], s=40, marker=markers[i])


plt.title('Decision Boundary by Random Forest', fontsize=14)
plt.show()



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 Python의 NLTK (Natural Language Toolkit) 라이브러리WordNet 말뭉치를 사용하여 자연어 처리 (Natural Language Processing) 하는 몇 가지 방법을 맛보기로 소개하겠습니다. 

(저는 Python 3.8 버전에 NLTK 3.5 버전을 사용하였습니다. 자연어 처리, NLTK를 제대로 살펴보려면 책 한권, 한 학기 수업 분량이며, 이번 포스팅은 말 그대로 맛보기 정도의 소개입니다. ^^;)


Python NLTK (Natural Language Toolkit) 라이브러리는 텍스트 분류, 토큰화, 단어 stemming, 품사 태킹, 텍스트 파싱, semantic reasoning 등을 하는데 사용하는 텍스트 자연어 처리 파이썬 모듈입니다.

(* reference: https://www.nltk.org/book/ch02.html )


WordNet은 어휘에 중점을 둔 영어 사전 Database 로서, 전통적인 Thesaurus (유의어 사전)와 유사한 측면이 있지만 보다 풍성한 구조를 가지고 있습니다. 1985년부터 프린스턴 대학교에서 구축하기 시작하였으며, 약 16만개의 단어(155287 words)와 12만개의 유사어 집합(117,659 synonym sets)을 포함하고 있는 방대한 Thesaurus 입니다. 

(* reference: https://www.nltk.org/howto/wordnet.html )





NLTK와 WordNet에 대한 간단한 소개를 마쳤으니 이제 직접 코드를 짜보면서 예를 들어보겠습니다. 먼저, 명령 프롬프트 창에서 pip install로 NLTK 라이브러리를 설치하고 pip show로 버전과 내용을 확인해보겠습니다. (Anaconda 배포판으로 Python 가상 환경을 만들었다면 아마 디폴트로 NLTK 모듈이 설치되어 있을 겁니다.)



-- (명령 프롬프트 창에서) NLTK 모듈 설치

$ pip install nltk


-- NLTK 모듈 확인

$ pip show nltk

Name: nltk

Version: 3.5

Summary: Natural Language Toolkit

Home-page: http://nltk.org/

Author: Steven Bird

Author-email: stevenbird1@gmail.com

License: Apache License, Version 2.0

Location: /Users/ihongdon/anaconda3/envs/py3.8_tf2.3/lib/python3.8/site-packages

Requires: regex, click, tqdm, joblib

Required-by: 




 (1) NLTK로 Word Tokenization 하기


Token 이란 더이상 나눌 수 없는 언어요소를 말하며, Tokenization은 텍스트 문자열을 Token으로 분해하여 쪼개는 것입니다. 


처음 사용하는 경우라면 먼저 nltk.download('punkt') 를 실행하여 Punket Tokenizer Models (13MB) 를 다운로드 해줍니다. 


nltk.word_tokenize() 함수를 사용해서 괄호 안에 텍스트 문자열 객체를 넣어주면 Token으로 쪼개줍니다. 

(영어는 띄어쓰기 기준으로 단어 토큰화가 되었는데요, 한글은 단순히 띄어쓰기 기준만으로 토큰화가 되지 않는 어려움이 있습니다.)



#%% word tokenization


import nltk


# Please use the NLTK Downloader to obtain the resource:

nltk.download('punkt')


sentense = "NLTK is a leading platform for building Python programs to work with human language data."


tokens = nltk.word_tokenize(sentense)

tokens

Out[1]: 

['NLTK',

 'is',

 'a',

 'leading',

 'platform',

 'for',

 'building',

 'Python',

 'programs',

 'to',

 'work',

 'with',

 'human',

 'language',

 'data',

 '.']

 


* 참고로, keras 라이브러리로의 text_to_word_seqence() 메소드로 단어 토큰화를 할 수 있습니다. 

(from tensorflow.keras.preprocessing.text import text_to_word_sequence)




  (2) NLTK로 품사 태깅하기 (Tagging)


품사 태깅을 하려면 먼저 nltk.download('averaged_perceptron_tagger') 로 태깅에 필요한 자원을 다운로드 해줍니다. 그리고 nltk.pos_tag() 메소드를 사용해서 위의 (1)번에서 만든 단어 토큰들에 대해서 품사 태킹을 할 수 있습니다. 



#%% word tagging


# Please use the NLTK Downloader to obtain the resource:

nltk.download('averaged_perceptron_tagger')


tagged = nltk.pos_tag(tokens)

tagged

Out[2]: 
[('NLTK', 'NNP'),
 ('is', 'VBZ'),
 ('a', 'DT'),
 ('leading', 'VBG'),
 ('platform', 'NN'),
 ('for', 'IN'),
 ('building', 'VBG'),
 ('Python', 'NNP'),
 ('programs', 'NNS'),
 ('to', 'TO'),
 ('work', 'VB'),
 ('with', 'IN'),
 ('human', 'JJ'),
 ('language', 'NN'),
 ('data', 'NNS'),
 ('.', '.')]

 



위의 단어 태킹 예에서 보면, ('NLTK', 'NNP') 처럼 (단어 토큰, 품사) 의 쌍을 이룬 튜플들의 리스트로 되어있습니다. 품사의 약어가 무엇을 의미하는지는 아래의 품사 약어를 참고하시기 바랍니다. 

(예: ('is', 'VBZ') 에서 'VBZ'는 verb, 3rd person sing. present takes 입니다)



POS tag list:


CC coordinating conjunction

CD cardinal digit

DT determiner

EX existential there (like: "there is" ... think of it like "there exists")

FW foreign word

IN preposition/subordinating conjunction

JJ adjective 'big'

JJR adjective, comparative 'bigger'

JJS adjective, superlative 'biggest'

LS list marker 1)

MD modal could, will

NN noun, singular 'desk'

NNS noun plural 'desks'

NNP proper noun, singular 'Harrison'

NNPS proper noun, plural 'Americans'

PDT predeterminer 'all the kids'

POS possessive ending parent\'s

PRP personal pronoun I, he, she

PRP$ possessive pronoun my, his, hers

RB adverb very, silently,

RBR adverb, comparative better

RBS adverb, superlative best

RP particle give up

TO to go 'to' the store.

UH interjection errrrrrrrm

VB verb, base form take

VBD verb, past tense took

VBG verb, gerund/present participle taking

VBN verb, past participle taken

VBP verb, sing. present, non-3d take

VBZ verb, 3rd person sing. present takes

WDT wh-determiner which

WP wh-pronoun who, what

WP$ possessive wh-pronoun whose

WRB wh-abverb where, when


* source: https://pythonprogramming.net/part-of-speech-tagging-nltk-tutorial/ 





  (3) WordNet에서 동의어 (Synonyms) 찾기


WordNet을 처음 사용하는 사용자라면 먼저 nltk.download('wordnet') 을 다운로드 해줍니다. 그리고 from nltk.corpus import wordnet as wn 으로 WordNet을 wn 이라는 alias로 importing 해주었습니다. 


단어의 의미가 여러개가 있을 수 있는데요, NLTK WordNet에서 wordnet.synsets() 함수를 사용해서 동의어 집합을 찾을 수 있습니다. 아래 예에서는 'car'라는 단어가 5개의 단어 집합을 가지고 있네요. 



#%% Senses and Synonyms from Wordnet


# Please use the NLTK Downloader to obtain the resource:

nltk.download('wordnet')


from nltk.corpus import wordnet as wn

wn.synsets('car')

Out[3]: 

[Synset('car.n.01'),

 Synset('car.n.02'),

 Synset('car.n.03'),

 Synset('car.n.04'),

 Synset('cable_car.n.01')]




가령, Synset('car.n.01') 는 '단어.품사.그룹인덱스' 를 나타내는데요, 특정 의미의 단어를 보려면 '그룹인덱스'를 사용해서 명시적으로 지정을 해줘야 합니다. 아래 예에서는 첫번째 인덱스의 'car.n.01' 표제어의 c단어 정의(definition())와, 동의어 단어 집합(lemma_names())을 알아보겠습니다. 



car = wn.synset('car.n.01')

car.definition()

Out[4]: 'a motor vehicle with four wheels; usually propelled by an internal combustion engine'


car.lemma_names()

Out[5]: ['car', 'auto', 'automobile', 'machine', 'motorcar']




for loop 을 사용해서 car의 5개 표제어 모두에 대해서 차례대로 모두 동의어 단어집합을 인쇄해볼 수도 있습니다. 



for synset in wn.synsets('car'):

    print(synset.lemma_names())


['car', 'auto', 'automobile', 'machine', 'motorcar']

['car', 'railcar', 'railway_car', 'railroad_car']

['car', 'gondola']

['car', 'elevator_car']

['cable_car', 'car']





  (4) WordNet에서 반대말 (Antonyms) 찾기


위의 (3)번에서는 WordNet에서 동의어(Synonym)를 찾아보았다면, 이제는 WordNet에서 반대말, 반의어(Antonym)를 찾아보겠습니다. 가령, 아래 예에서는 공급(supply)의 반대말은 수요(demand), 왕(king)의 반대말은 여왕(queen) 이라고 대답해주네요. 



#%% antonym


wn.lemma('supply.n.02.supply').antonyms()

Out[7]: [Lemma('demand.n.02.demand')]


wn.lemma('king.n.01.king').antonyms()

Out[8]: [Lemma('queen.n.02.queen')]

 




  (5) WordNet에서 단어 위계구조 (Hierarchy) 찾기


WordNet에는 단어 간의 관계를 상/위계구조로 정리가 되어있습니다. 'car.n.01' 표제어에 대해서 상/하 위계구조를 hypernym_paths() 메소드를 사용해서 찾아보면 아래와 같이 2개가 나옵니다. car.hypernym_paths()[0] 으로 '0' 번째 것만 indexing 해서 볼 수 있습니다. 



#%% The Wordnet Hierarchy


# 상위어 (hypernym)

from nltk.corpus import wordnet as wn

car = wn.synset('car.n.01')


car.hypernym_paths()

Out[9]: 

[[Synset('entity.n.01'),

  Synset('physical_entity.n.01'),

  Synset('object.n.01'),

  Synset('whole.n.02'),

  Synset('artifact.n.01'),

  Synset('instrumentality.n.03'),

  Synset('container.n.01'),

  Synset('wheeled_vehicle.n.01'),

  Synset('self-propelled_vehicle.n.01'),

  Synset('motor_vehicle.n.01'),

  Synset('car.n.01')],

 [Synset('entity.n.01'),

  Synset('physical_entity.n.01'),

  Synset('object.n.01'),

  Synset('whole.n.02'),

  Synset('artifact.n.01'),

  Synset('instrumentality.n.03'),

  Synset('conveyance.n.03'),

  Synset('vehicle.n.01'),

  Synset('wheeled_vehicle.n.01'),

  Synset('self-propelled_vehicle.n.01'),

  Synset('motor_vehicle.n.01'),

  Synset('car.n.01')]]



# indexing

car.hypernym_paths()[0]

Out[10]: 

[Synset('entity.n.01'),

 Synset('physical_entity.n.01'),

 Synset('object.n.01'),

 Synset('whole.n.02'),

 Synset('artifact.n.01'),

 Synset('instrumentality.n.03'),

 Synset('container.n.01'),

 Synset('wheeled_vehicle.n.01'),

 Synset('self-propelled_vehicle.n.01'),

 Synset('motor_vehicle.n.01'),

 Synset('car.n.01')]

 



위에 위계구조 리스트를 상/하 네트워크 그래프로 시각화해서 보면 좀더 직관적으로 이해할 수 있습니다. 이때 노드(nodes, vertex)는 동의어 집합(synsets)에 해당하며, 연결선(edges, links)은 단어 개념 상의 상/하 관계(hypernym/hyponym relation)을 나타냅니다. 



* image source: https://www.nltk.org/book/ch02.html




  (6) WordNet을 이용한 단어 간 의미 유사도 (Semantic Similarity) 측정


단어 간 의미 유사도를 측정하는 방법에는 다양한 기준이 있을 수 있는데요, 아래의 3가지 방법을 소개하겠습니다. 

  • 경로 거리 기반 유사도 (Path Distance Similarity)
  • Leacock Chordorow 유사도 (Leacock Chordorow Similarity)
  • Wu-Palmer 유사도 (Wu-Palmer Similarity)


(6-1) 경로 거리 기반 유사도 (Path Distance Similarity)


먼저, 경로 거리 유사도는 위의 (5)번에서 소개했던 단어 간 상/하 위계구조에서의 최단 경로 (Shortest Path)의 거리를 기반으로 유사도를 0~1 사이의 실수로 측정합니다. (즉, 경로 거리가 가까울 수록 유사하고, 거리가 멀 수록 유사하지 않게 평가하며, 유사도가 1에 가까울 수록 유사한 것을 의미함). 


아래 예에서는 고래 right whale 과 orca, minke whale 는 경로 거리 유사도가 높게 나왔고, 거북이 tortoise 는 좀 낮게 나왔으며, 소설 novel 은 매우 낮게 나왔습니다. 상식과 어느정도 부합하네요. 



#%% Semantic Similarity


# (1) Path Distance Similarity

right_whale = wn.synset('right_whale.n.01')

orca = wn.synset('orca.n.01')

right_whale.path_similarity(orca)

Out[11]: 0.16666666666666666


minke = wn.synset('minke_whale.n.01')

right_whale.path_similarity(minke)

Out[12]: 0.25


tortoise = wn.synset('tortoise.n.01')

right_whale.path_similarity(tortoise)

Out[13]: 0.07692307692307693


novel = wn.synset('novel.n.01')

right_whale.path_similarity(novel)

Out[14]: 0.043478260869565216




(6-2) Leacock Chordorow 유사도 (Leacock Chordorow Similarity)


 Leacock Chodorow 유사도는 단어 간 위계구조에서의 최단 거리(shortest path)와 단어 의미가 발생하는 최대 깊이(maximum depth)에 기반해서 유사도를 계산합니다. 위의 path_similarity() 가 0~1 사이의 표준화된 값을 반환하는 반면에, lch_similarity() 는 표준화되지 않은 차이가 있습니다. 

아래 예에서 right whale 과 minke whale, tortoise, novel 간의 Leacock Chordorow 유사도와 위의 (6-1) Path distance similarity 의 경향, 유사도 순서는 비슷하게 나왔습니다.  



# (3) Leacock Chordorow (LCH) Similarity

right_whale.lch_similarity(orca)

Out[15]: 1.845826690498331


right_whale.lch_similarity(minke)

Out[16]: 2.2512917986064953


right_whale.lch_similarity(tortoise)

Out[17]: 1.072636802264849


right_whale.lch_similarity(novel)

Out[18]: 0.5020919437972361

 



(6-3) Wu-Palmer 유사도 (Wu-Palmer Similarity)


Wu-Palmer 유사도는 단어 위계구조에서 두 단어의 깊이(depth of the tow senses in the taxonomy)와 단어 간의 최소 공통 포함(Least Common Subsumer)에 기반해서 유사도를 계산합니다. 



# (2) Wu-Palmer Similarity

right_whale.wup_similarity(orca)

Out[19]: 0.8484848484848485


right_whale.wup_similarity(minke)

Out[20]: 0.9090909090909091


right_whale.wup_similarity(tortoise)

Out[21]: 0.6


right_whale.wup_similarity(novel)

Out[22]: 0.08333333333333333




위의 3가지 외에도 NLTK 라이브러리는 최소 공통 포함 Information Content (IC of the Least Common Subsumer) 기반의 Resnik Similarity, Lin Similarity, Jiang-Conrath Similarity 등의 알고리즘을 제공합니다. (* reference: http://jaganadhg.github.io/wornet-nltk-sense/)


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 A, B 두개의 값이 들어있는 배열과 위치를 지정해둔 idx 배열이 있다고 했을 때, A배열의 idx 배열 내 index 위치에다가 B배열의 원소를 순서대로 더하기하는 두가지 방법을 소개하겠습니다. 


(방법 1) for loop 으로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기

(방법 2) np.add.at(A, idx, B) 메소드로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기


for loop을 이용한 (방법 1)은 numpy의 add.at() 함수를 몰라도 어렵지 않게 구현할 수 있고 또 코드를 해석하기도 편한 장점이 있습니다만, 만약 더해야 하는 원소의 개수가 많아지면 for loop 연산을 하는데 시간이 많이 소요되는 단점이 있습니다. 


numpy의 add.at() 메소드를 사용하는 (방법 2)는 Vectorization 연산을 하여 한꺼번에 두 배열 간 idx 위치에 맞게 더하기 연산을 수행하므로 위의 for loop 방법 1 대비 빠르다는 장점이 있습니다. (더해야 하는 원소 개수가 작으면 방법1과 방법2의 속도 차이를 느끼기 힘든데요, 만약 몇 몇 백만개라면 for loop으로 수 분~ 수 시간 걸릴 것이 numpy.add.at() 메소드로는 수 초안에 끝낼 수도 있을만큼 성능 차이가 많이 날 것입니다. (대신 numpy.add.at() 함수를 기억하고 사용법도 알고 있어야 하겠지만요.)





  (방법 1) for loop 으로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기


먼저, 예제로 사용할 A배열, B배열, idx 배열을 만들어보겠습니다. 



import numpy as np


A = np.arange(24).reshape(8, 3)

print(A)

[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]
 [12 13 14]
 [15 16 17]
 [18 19 20]
 [21 22 23]]


B = np.arange(12).reshape(4, 3)

print(B)

[[ 0  1  2]
 [ 3  4  5]
 [ 6  7  8]
 [ 9 10 11]]


idx = np.array([0, 0, 3, 5])




자, 이제 준비가 되었으니 for loop 문과 enumerate() 함수를 사용하여 A배열의 idx 배열 내 숫자의 위치에다가 B배열의 원소를 순서대로 더해보겠습니다. 

idx 가 array([0, 0, 2, 5]) 이므로 A배열의 1차원의 idx([0, 0, 3, 5]) 번째 위치(0, 0, 3, 5)의 원소에 B배열의 0, 1, 2, 3 번째 위치의 원소를 더하는 연산을 수행하게 됩니다. 


말로만 설명을 들으면 좀 헷갈리고 이해가 잘 안갈수도 있는데요, 아래에 배열 덧셈 연산식과 연산이 끝난 A배열에서 결과값이 바뀐 0, 2, 5 번째 원소를 빨간색으로 표시해두었으니 참고하기 바랍니다. (* idx([0, 0, 3, 5]) 에서 A[0]에 B[0]과 B[1]이 각 각 더해지는 것에 주의)


array A at idx([0])  + array B[0]          = array A[0]

array([0, 1, 2])        + array([0, 1, 2])    = array([0, 2, 4])


array A at idx([0])  + array B[1         = array A[0]

array([0, 2, 4])      + array([3, 4, 5])     = array([3, 6, 9])


array A at idx([3])  + array B[2]          = array A[3]

array([9, 10, 11])     + array([6, 7, 8])    = array([15, 17, 19])


array A at idx([5])  + array B[3]          = array A[5]

array([15, 16, 17])   + array([9, 10, 11])  = array([24, 25, 26])



for i, id in enumerate(idx):

    A[id] += B[i]


print(A)

[[ 3  6  9]
 [ 3  4  5]
 [ 6  7  8]
 [15 17 19]
 [12 13 14]
 [24 26 28]
 [18 19 20]
 [21 22 23]]

 




  (방법 2) np.add.at(A, idx, B) 함수로 A배열의 idx 위치에 B배열의 원소 순서대로 더하기


위의 for loop을 이용한 (방법 1)과 똑같은 연산을 numpy의 add.at(A, idx, B) 메소드를 이용해서 수행하면 아래와 같습니다. 코드도 깔끔하고 연산속도도 더 빠르므로 특히 대용량 배열을 다루어야 하는 경우라면 알아두면 좋겠습니다. 



import numpy as np


A = np.arange(24).reshape(8, 3)

B = np.arange(12).reshape(4, 3)

idx = np.array([0, 0, 3, 5])


np.add.at(A, idx, B)


print(A)

[[ 3  6  9]
 [ 3  4  5]
 [ 6  7  8]
 [15 17 19]
 [12 13 14]
 [24 26 28]
 [18 19 20]
 [21 22 23]]

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. ^_^


Posted by R Friend Rfriend

댓글을 달아 주세요

이번 포스팅에서는 Python pandas 데이터프레임 안에 여러개의 칼럼 별로 결측값을 대체하는 방법(how to fill missing values per each columns)을 다르게 하는 방법을 소개하겠습니다. 




먼저 예제로 사용할 간단할, 각 칼럼별로 결측값을 포함하고 있는 pandas DataFrame을 만들어보겠습니다. 



import numpy as np

import pandas as pd


# Make a DataFrame with missing values

df = pd.DataFrame({'a': [1, 2, 3, np.nan, 5], 

                   'b': [30, 20, np.nan, 35, 32], 

                   'c': [0.2, np.nan, 0.5, 0.3, 0.4], 

                   'd': ['c1', np.nan, 'c3', 'c4', 'c5'], 

                   'e': [10, 11, np.nan, 13, 15]})

 

df

abcde
01.030.00.2c110.0
12.020.0NaNNaN11.0
23.0NaN0.5c3NaN
3NaN35.00.3c413.0
45.032.00.4c515.0




다음으로, 각 칼럼별로 결측값을 대체하는 방법을 Dictionary에 Key (칼럼 이름): Value (결측값 대체 방법/값) 로 정리해보겠습니다. 


[ 칼럼별 결측값 대체 방법(전략) ]

  • 칼럼 'a': 0 으로 결측값 대체
  • 칼럼 'b': 평균(mean)으로 결측값 대체
  • 칼럼 'c': 중앙값(median)으로 결측값 대체
  • 칼럼 'd': 'Unknown'으로 결측값 대체
  • 칼럼 'e': 결측값 보간 (interpolation)



missing_fill_val = {'a': 0, 

                    'b': df.b.mean(), 

                    'c': df.c.median(), 

                    'd': 'Unknown', 

                    'e': df.e.interpolate()}


print(missing_fill_val)

{'a': 0, 'c': 0.35, 'b': 29.25, 'e': 0    10.0
1    11.0
2    12.0
3    13.0
4    15.0
Name: e, dtype: float64, 'd': 'Unknown'}

 



이제 준비가 다 되었으니 df DataFrame의 각 칼럼별로 서로 다른 결측값 대체 전략을 사용하여 결측값을 채워보겠습니다.  fillna() 함수의 괄호 안에 위에서 정의한 Dictionary 를 넣어주면 끝입니다. 간단하지요? ^^



df2 = df.fillna(missing_fill_val)


df2


a
bcde
01.030.000.20c110.0
12.020.000.35Unknown11.0
23.029.250.50c312.0
30.035.000.30c413.0
45.032.000.40c515.0

 



만약 원본 df DataFrame 을 보존할 필요가 없이 바로 결측값을 채워넣기 해서 수정하고 싶으면 inplace=True 옵션을 설정해주면 됩니다. 



df.fillna(missing_fill_val, inplace=True)

abcde
01.030.000.20c110.0
12.020.000.35Unknown11.0
23.029.250.50c312.0
30.035.000.30c413.0
45.032.000.40c515.0

 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요.



Posted by R Friend Rfriend

댓글을 달아 주세요

이전 포스팅에서 Python으로 JSON 데이터 읽고 쓰기, XML 데이터 읽고 쓰기에 대해서 소개한 적이 있습니다.  이번 포스팅에서는 Python의 PyYAML 라이브러리를 이용하여 YAML 파일을 파싱하여 파이썬 객체로 읽어오기, Python 객체를 YAML 파일로 쓰는 방법을 소개하겠습니다. 



YAML에 대한 Wikipedia의 소개를 먼저 살펴보겠습니다. 

YAML 은 "YAML Ain't Markup Language" 의 반복적인 약어(recursive acronym)로서, 인간이 읽을 수 있는 데이터 직렬화 언어(human-readable data-serialization language) 입니다. YAML 은 데이터가 저장되거나 전송되는 구성 파일(configuration file)과 애플리케이션에서 종종 사용됩니다. YAML은 XML 과 동일한 커뮤니케이션 애플리케이션을 대상으로 하지만 최소한의 구문을 가지고 있습니다. 

* source: https://en.wikipedia.org/wiki/YAML


아래는 데이터가 저장되거나 전송되는 구성파일을 XML과 JSON, YAML으로 나타내서 비교한 예입니다. Python 들여쓰기(indentation) 형태로 해서 XML 이나 JSON 대비 Syntax 가 매우 간소해졌음을 알 수 있습니다. 




Python 에서 YAML 파일을 파싱하거나, 반대로 Python 객체를 YAML 파일로 내보낼 때는 PyYAML 라이브러리 (PyYAML is YAML parser and emitter for python)를 사용합니다. 이전에 XML이나 JSON을 다루었을 때와 PyYAML 라이브러리의 load(), dump() 함수 사용법은 비슷합니다. 





먼저, PyYAML 라이브러리가 설치되어 있지 않다면 명령 프롬프트에서 pip로 설치를 해주면 됩니다. 




  (1) YAML 파일을 파싱해서 Python 객체로 읽어오기: yaml.load()


예제로 사용한 YAML 파일은 아래처럼 'Vegetables' 키에 'Pepper', 'Tamato', 'Garlic' 을 값으로 가지는 YAML 파일(vegetables.yml)입니다. (Notepad++ 에디터에서 파일 형식을 YAML로 해서 작성하면 들여쓰기를 알아서 맞추어 줍니다. PyCharm 같은 프로그래밍 에디터를 사용해도 편리합니다.)



vegetables.yml


with open() 으로 'vegetables.yml' YAML 파일을 연 후에, yaml.load() 함수로 YAML 파일을 파싱하여 vegetables 라는 이름의 Python 객체로 저장하였습니다. Python 객체를 인쇄해보면 Key, Value (list) 로 구성된 Dictionary 로 YAML 파일을 파싱했음을 알 수 있습니다. 



import yaml


with open('vegetables.yml') as f:

    vegetables = yaml.load(f, Loader=yaml.FullLoader)

    print(vegetables)

 

{'Vegetables': ['Pepper', 'Tomato', 'Garlic']}




아래와 같이 Kubernetes의 deployment-definition.yaml 처럼 조금 복잡한 YAML 파일을 PyYAML 로 파싱해보면 List와 Nested Dictionary 로 구성된 Dictionary로 파싱합니다. 



k8s_deployment_yaml.yml




import yaml


with open('deployment-definition.yml') as f:

    deployment_def = yaml.load(f, Loader=yaml.FullLoader)


deployment_def

{'apiVersion': 'apps/v1',
 'kind': 'Deployment',
 'metadata': {'name': 'frontend',
  'labels': {'app': 'mywebsite', 'tier': 'frontend'}},
 'spec': {'replicas': 3,
  'template': {'metadata': {'name': 'myapp-pod', 'labels': {'app': 'myapp'}},
   'spec': {'containers': [{'name': 'nginx', 'image': 'nginx'}]}},
  'selector': {'matchLabels': {'app': 'myapp'}}}}





  (2) 여러개의 YAML 문서들을 파싱하여 읽어오기 : yaml.load_all()


YAML 문서를 구분할 때는 '---' 를 사용합니다. 아래는 'Fruits'와 'Vegetables' 의 두개의 YAML 문서를 '---'로 구분해서 하나의 YAML 파일로 만든 것입니다. 



예제 파일:

fruit-vegetable.yml



위의 (1)번에서는 1개의 YAML 문서를 yaml.load() 함수로 Python으로 읽어왔었다면, 이번의 (2)번에서는 '---'로 구분되어 있는 여러개의 YAML 문서를 yaml.load_all() 함수를 사용해서 Python 객체로 파싱하여 읽어오겠습니다. 



import yaml


with open('fruit-vegetable.yml') as f:

    

    fruits_vegetables = yaml.load_all(f, Loader=yaml.FullLoader)

    

    for fruit_vegetable in fruits_vegetables:

        print(fruit_vegetable)

 

{'Fruits': ['Blueberry', 'Apple', 'Orange']}
{'Vegetables': ['Pepper', 'Tomato', 'Garlic']}




  (3) 읽어온 YAML 파일을 정렬하기


아래와 같이 자동차 브랜드, 가격 쌍으로 이우러진 cars.yml YAML 파일을 Python 객체로 파싱해서 읽어올 때 Key 기준, Value 기준으로 각각 정렬을 해보겠습니다. (Dictionary 정렬 방법 사용)



cars.yml


(3-1) 읽어온 YAML 파일을 Key 기준으로 정렬하기 (sorting by Key)

    : 방법 1) yaml.dump(object, sort_keys=True) 



import yaml


with open('cars.yml') as f:

    

    cars_original = yaml.load(f, Loader=yaml.FullLoader)

    print(cars_original)

    

    print("---------------------")

    

    # sorting by Key

    cars_sorted = yaml.dump(cars_original, sort_keys=True)

    print(cars_sorted)

 

{'hyundai': 45000, 'tesla': 65000, 'chevrolet': 42000, 'audi': 51000, 'mercedesbenz': 80000}
---------------------
audi: 51000
chevrolet: 42000
hyundai: 45000
mercedesbenz: 80000
tesla: 65000



  : 방법 2) sorted(object.items()) 메소드 사용 



import yaml


with open('cars.yml') as f:

    

    cars_original = yaml.load(f, Loader=yaml.FullLoader)

    print(cars_original)

    

    print("---------------------")

    # sort by key in ascending order

    for key, value in sorted(cars_original.items()):

        print(key, ':', value


{'hyundai': 45000, 'tesla': 65000, 'chevrolet': 42000, 'audi': 51000, 'mercedesbenz': 80000}
---------------------
audi : 51000
chevrolet : 42000
hyundai : 45000
mercedesbenz : 80000
tesla : 65000

 



(3-2) Key 값의 역순으로 정렬 (sorting in reverse order): sorted(object.items(), reverse=True)



import yaml


with open('cars.yml') as f:

    cars_original = yaml.load(f, Loader=yaml.FullLoader)

    print(cars_original)

    

    print("---------------------")

    

    # sorting by key in reverse order

    for key, value in sorted(cars_original.items(), reverse=True):

        print(key, ':', value)


tesla : 65000
mercedesbenz : 80000
hyundai : 45000
chevrolet : 42000
audi : 51000




(3-3) 읽어온 YAML 파일을 Value 기준으로 정렬하기 (sorting by Value)



import yaml


with open('cars.yml') as f:

    cars_original = yaml.load(f, Loader=yaml.FullLoader)

    print(cars_original)

    

    print("---------------------")

    # sorting by value in ascending order

    for key, value in sorted(cars_original.items(), key = lambda item: item[1]):

        print(key, ':', value)


{'hyundai': 45000, 'tesla': 65000, 'chevrolet': 42000, 'audi': 51000, 'mercedesbenz': 80000}
---------------------
chevrolet : 42000
hyundai : 45000
audi : 51000
tesla : 65000
mercedesbenz : 80000




  (4) Python 객체를 YAML stream으로 직렬화 하기: yaml.dump()


Key, Value 쌍으로 이루어진 Python Dictionary를 yaml.dump() 메소드를 사용해서 YAML stream으로 직렬화해보겠습니다. 



import yaml


fruits = {'fruits': ['blueberry', 'apple', 'orange']}


# serialize a Python object into YAML stream

fruits_serialized_yaml = yaml.dump(fruits)

print(fruits_serialized_yaml)

 

fruits:
- blueberry
- apple
- orange




  (5) Python 객체를 YAML 파일로 쓰기: with open('w') as f: yaml.dump()


위의 (4)번에서 소개한, YAML stream으로 직렬화하는 yaml.dump() 메소드에 with open('w') 함수를 같이 사용해서 이번에는 YAML file 에 쓰기('w')를 해보겠습니다. 



import yaml


fruits = {'fruits': ['blueberry', 'apple', 'orange']}


with open('fruits.yaml', 'w') as f:

    yaml.dump(fruits, f)




많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. 



Posted by R Friend Rfriend

댓글을 달아 주세요

  1. 애드센스 원정대 2020.06.29 06:55 신고  댓글주소  수정/삭제  댓글쓰기

    유익한 내용이네요. 잘 봤습니다.

  2. 민거리 2021.03.10 20:59  댓글주소  수정/삭제  댓글쓰기

    여러가지 선언법을 예시로 들어주셔서
    도움이 많이 되었습니다!

이번 포스팅에서는 (1) 날짜 TimeStamp (m개) 와 (2) 고객 ID (n개) 의 두 개 칼럼을 가진 DataFrame에서 고객ID 별로 중간 중간 날짜 TimeStamp 가 비어있는 경우 모두 '0'으로 채워서 모든 조합 (m * n 개) 을 MultiIndex로 가진 시계열 데이터 형태의 DataFrame을 만들어보겠습니다. 


이번 포스팅에서는 pandas DataFrame에서 index 설정 관련해서 MultiIndex(), set_index(), reindex(), reset_index() 등 index 설정과 재설정 관련된 여러가지 메소드가 나옵니다. 


말로만 설명을 들으면 좀 어려운데요, 아래의 전(Before) --> 후(After) DataFrame의 전처리 Output Image를 보면 이해하기가 쉽겠네요. (시계열 데이터 분석을 할 때 아래의 우측에 있는 것처럼 데이터 전처리를 미리 해놓아야 합니다.) 




먼저, 년-월-일 TimeStamp (ts), 고객 ID (id), 구매 금액 (amt)의 세개 칼럼으로 구성된 거래 데이터(tr)인, 예제로 사용할 간단한 pandas DataFrame을 만들어보겠습니다. 



import pandas as pd


tr = pd.DataFrame({

    'ts': ['2020-06-01', '2020-06-02', '2020-06-03', '2020-06-01', '2020-06-03'], 

    'id': [1, 1, 1, 2, 3], 

    'amt': [100, 300, 50, 200, 150]})


tr

tsidamt
02020-06-011100
12020-06-021300
22020-06-03150
32020-06-012200
42020-06-033150

 



다음으로, 거래 데이터(tr) DataFrame의 날짜(ts)와 고객ID(id)의 모든 조합(all combination)으로 구성된  Multi-Index 를 만들어보겠습니다. pd.MultiIndex.from_product((A, B)) 메소드를 사용하면 Cartesian Product 을 수행하여, 총 A의 구성원소 개수 * B의 구성원소 개수 종류 만큼의 MultiIndex 를 생성해줍니다. 위 예제의 경우 날짜(ts)에 '2020-06-01', '2020-06-02', '2020-06-03'의 3개 날짜가 있고, 고객ID(id) 에는 1, 2, 3 의 3개가 있으므로 Cartesian Product 을 하면 아래의 결과처럼 3 * 3 = 9 의 조합이 생성이 됩니다. 



date_id_idx = pd.MultiIndex.from_product((set(tr.ts), set(tr.id)))

date_id_idx

MultiIndex([('2020-06-01', 1),
            ('2020-06-01', 2),
            ('2020-06-01', 3),
            ('2020-06-02', 1),
            ('2020-06-02', 2),
            ('2020-06-02', 3),
            ('2020-06-03', 1),
            ('2020-06-03', 2),
            ('2020-06-03', 3)],
           )



이제 위에서 Cartesian Product으로 만든 TimeStamp(ts)와 고객ID(id)의 모든 조합으로 구성된 MultiIndex인 date_id_idx 를 사용하여 index를 재설정(reindex) 해보겠습니다. 이때 원래(Before)의 DataFrame에는 없었다가 date_id_idx 로 index를 재설정(reindex) 하면서 새로 생긴 행에 구매금액(amt) 칼럼에는 'NaN' 의 결측값이 들어가게 됩니다. 이로서 처음에 5개 행이었던 것이 이제 9개(3*3=9) 행으로 늘어났습니다. 



tr_tsformat = tr.set_index(['ts', 'id']).reindex(date_id_idx)

tr_tsformat

amt
2020-06-011100.0
2200.0
3NaN
2020-06-021300.0
2NaN
3NaN
2020-06-03150.0
2NaN
3150.0

 



날짜(ts)와 고객ID(id)의 MultiIndex로 reindex() 하면서 생긴 NaN 값을 '0'으로 채워넣기(fill_value=0)해서 새로 DataFrame을 만들어보겠습니다. 



tr_tsformat = tr.set_index(['ts', 'id']).reindex(date_id_idx, fill_value=0)

tr_tsformat

amt
2020-06-011100
2200
30
2020-06-021300
20
30
2020-06-03150
20
3150

 



만약 날짜(ts)와 고객ID(id)의 MultiIndex로 이루어진 위의 DataFrame에서 MultiIndex를 칼럼으로 변경하고 싶다면 reset_index() 함수를 사용하면 됩니다. 칼럼 이름은 애초의 DataFrame과 동일하게 ['ts', 'id', 'amt'] 로 다시 부여해주었습니다. 



tr_tsformat.reset_index(inplace=True)

tr_tsformat

level_0level_1amt
02020-06-011100
12020-06-012200
22020-06-0130
32020-06-021300
42020-06-0220
52020-06-0230
62020-06-03150
72020-06-0320
82020-06-033

150

 


tr_tsformat.columns = ['ts', 'id', 'amt']

tr_tsformat

tsidamt
02020-06-011100
12020-06-012200
22020-06-0130
32020-06-021300
42020-06-0220
52020-06-0230
62020-06-03150
72020-06-0320
82020-06-033150




참고로, pandas에서 ID는 없이 TimeStamp만 있는 일정한 주기의 시계열 데이터 Series, DataFrame 만들기는 https://rfriend.tistory.com/501 를 참고하세요. 



많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)


Posted by R Friend Rfriend

댓글을 달아 주세요

Python은 고수준의 객체지향 프로그래밍 언어입니다. 파이썬 툴을 사용해 재사용 가능한 코드를 작성하는 기능은 파이썬이 가지고 있는 큰 장점 중의 하나입니다. 


Python Functools 는 고차함수(high order functions)를 위해 설계된 라이브러리로서, 호출 가능한 기존의 객체, 함수를 사용하여 다른 함수를 반환하거나 활용할 수 있게 해줍니다. Functools 라이브러리는 기존 함수를 이용하는 함수를 모아놓은 모듈이라고 하겠습니다. Functools 라이브러리에는 partial(), total_ordering(), reduce(), chched_perperty() 등 여러 함수가 있는데요, 이번 포스팅에서는 partial() 메소드에 대해서 소개하겠습니다. 

(* functools.reduce() 함수 참고 : https://rfriend.tistory.com/366)





Functools partial 함수는 기존 파이썬 함수를 재사용하여 일부 위치 매개변수 또는 키워드 매개변수를 고정한(freezed, fixec) 상태에서, 원래의 함수처럼 작동하는 새로운 부분 객체(partial object)를 반환합니다. functools.partial() 함수의 syntax는 아래와 같이, 기존 함수이름을 써주고, 고정할 매개변수 값을 써주면 됩니다.  


from functools import partial


functools.partial(func, /, *args, **keywords)


간단한 예제로서, 숫자나 문자열을 정수(integer)로 변환해주는 파이썬 내장 함수 int(x, base=10)를 재활용하여, base=2 로 고정한 새로운 함수를 functools.partial() 함수로 만들어보겠습니다. 



# python build-in function int(x, base=10)

int('101', base=2)

[Out]: 5

int('101', base=5)

[Out]: 26

 



# create a basetwo() function using functools.partial() and int() function

from functools import partial


basetwo = partial(int, base=2)  # freeze base=2

basetwo('101')

[Out]: 5




int() 함수를 재활용해서 base=2 로 고정해서 새로 만든 basetwo() 라는 이름의 함수에 basetwo.__doc__ 로 설명을 추가하고, 호출해서 확인해보겠습니다. 



# add __doc__ attribute

basetwo.__doc__ = 'Convert base 2 string to an int.'


basetwo.__doc__

[Out]: 'Convert base 2 string to an int.'




functools.partial() 로 만든 새로운 객체는 func, args, keywords 속성(attributes)을 가지고 있습니다. 

이번 예에서는 재활용한 기존 함수(func)가 int(), 키워드 매개변수(keywords)가 base=2 ({'base': 2} 였는데요, 아래처럼 확인할 수 있습니다. 



basetwo.func

[Out]: int


basetwo.args

[Out]: ()


basetwo.keywords

[Out]: {'base': 2}

 



* functools library refreence: https://docs.python.org/3/library/functools.html#module-functools


많은 도움이 되었기를 바랍니다. 

이번 포스팅이 도움이 되었다면 아래의 '공감~'를 꾹 눌러주세요. :-)



Posted by R Friend Rfriend

댓글을 달아 주세요