시계열 데이터를 분석할 때 꼭 확인하고 처리해야 하게 있는데요, 바로 결측값 여부 확인과 결측값 처리입니다. 

 

시계열 데이터의 결측값을 처리하는 방법에는

   (1) 보간 (Interpolation)

   (2) 이전 값 또는 다음 값 이용 (previous/next value)

   (3) 이동 평균 (Moving average)

등의 여러가지 방법이 있습니다. 

 

 

[ 시계열 데이터 결측값 처리 방법 (How to handle the time series missing data) ]

 

 

아래의 보간(Interpolation)에 대한 내용은 Wikipedia 의 내용을 번역하여 소개합니다. 

 

데이터 분석의 수학 분야에서는 "보간법(Interpolation)을 이미 알려진 데이터 포인트들의 이산형 집합의 범위에 기반해서 새로운 데이터 포인트들을 만들거나 찾는 추정(estimation)의 한 유형"으로 봅니다. 

 

공학과 과학 분야에서는 종종 샘플링이나 실험을 통해서 많은 수의 데이터 포인트들을 획득하는데요, 이들 데이터는 어떤 함수(a function)의 값이나 독립변수(independent variable)의 제한적인 수의 값을 표현한 것입니다. 종종 독립변수의 중간 사이의 값을 위한 함수의 값을 추정(estimate the value of that function for an intermediat value of the independent variable)하는 보간이 필요합니다.  

 

밀접하게 관련된 문제로서 복잡한 함수를 간단한 함수로 근사하게 추정(the approximation of a complicated function by a simple function)하는 것이 있습니다. 어떤 주어진 함수의 공식이 알려져있지만, 너무 복잡해서 효율적으로 평가하기가 어렵다고 가정해봅시다. 원래의 함수로부터 적은 수의 새로운 데이터 포인트는 원래의 값과 상당히 근접한 간단한 함수를 생성해서 보간할 수 있습니다. 단순성(simplicity)으로부터 얻을 수 있는 이득이 보간에 의한 오차라는 손실보다 크고, 연산 프로세스면서도 더 좋은 성능(better performance in calculation process)을 낼 수도 있습니다.   

 

 

이번 포스팅에서는 Python scipy 모듈을 이용해서 시계열 데이터 결측값을 보간(Interpolation)하는 방법을 소개하겠습니다. 

 

1. 이전 값/ 이후 값을 이용하여 결측값 채우기 (Imputation using the previous/next values)

2. Piecewise Constant Interpolation

3. 선형 보간법 (Linear Interpolation)

4. 스플라인 보간법 (Spline Interpolation)

 

 

[ Python scipy 모듈을 이용한 결측값 보간 (Interpolation using Python scipy module)  ]

 

 

먼저 '0.5'로 동일한 간격을 가지는 x 값들에 대한 사인 함수 (sine function) 의 y값을 계산해서  예제 데이터로 사용하겠습니다. 아래 예졔의 점과 점 사이의 값들이 비어있는 결측값이라고 간주하고, 이들 값을 채워보겠습니다. 

 

import numpy as np
from scipy import interpolate
import matplotlib.pyplot as plt

## generating the original data with missing values
x = np.arange(0, 4*np.pi, 0.5)
y = np.sin(x)

plt.plot(x, y, "o")
plt.show()

original data with missing values

 

 

1. 이전 값/ 다음 값을 이용하여 결측값 채우기 (Imputation using the previous/next values)

 

데이터 포인트 사이의 값을 채우는 가장 간단한 방법은 이전 값(previous value) 나 또는 다음 값(next value)을 이용하는 것입니다. 함수를 추정하는 절차가 필요없으므로 연산 상 부담이 적지만, 데이터 추정 오차는 단점이 될 수 있습니다. 

 

## Interpolation using the previous value
f_prev = interpolate.interp1d(
    x, y, kind='previous') # next
y_new_prev = f_prev(xnew)

plt.plot(x, y, "o", xnew, y_new_prev, '-')
plt.show()

interpolation using the previous value

 

 

 

2. Piecewise Constant Interpolation

 

위 1번의 이전 값 또는 다음 값을 이용한 사이값 채우기를 합쳐놓은 방법입니다. Piecewise Constant Interpolation은 특정 데이터 포인트를 기준으로 가장 가까운 값 (nearest value) 을 가져다가 사이값을 보간합니다. ("최근접 이웃 보간"이라고도 함)

 

간단한 문제에서는 아래 3번에서 소개하는 Linear Interpolation 이 주로 사용되고, Piecewise Constant Interpolation 은 잘 사용되지 않는 편입니다. 하지만 다차원의 다변량 보간 (in higher-dimensional multivariate interpolation)의 경우, 속도와 단순성(speed and simplicity) 측면에서 선호하는 선택이 될 수 있습니다. 

 

## Piecewise Constant Interpolation
f_nearest = interpolate.interp1d(
    x, y, kind='nearest')

y_new_nearest = f_nearest(xnew)

plt.plot(x, y, "o", xnew, y_new_nearest)
plt.show()

Piecewise constant interpolation

 

 

 

3. Linear Interpolation

 

선형 보간법은 가장 쉬운 보간법 중의 하나로서, 연산이 빠르고 쉽습니다. 하지만 추정값이 정확한 편은 아니며, 데이터 포인트 Xk 에서 미분 가능하지 않다는 단점도 있습니다. 

 

일반적으로, 선형 보간법은 두 개의 데이터 포인트, 가령 (Xa, Ya)와 (Xb, Yb), 를 사용해서 다음의 공식으로 두 값 사이의 값을 보간합니다. 

 

Y = Ya + (Yb - Ya) * (X - Xa) / (Xb- Xa)    at the point (x, y)

 

## Linear Interpolation
f_linear = interpolate.interp1d(
    x, y, kind='linear')

y_new_linear = f_linear(xnew)

plt.plot(x, y, "o", xnew, y_new_linear, '-')
plt.show()

Linear interpolation

 

 

 

4. Spline Interpolation

 

다항식 보간법(Polynomial Interpolation)은 선형 보간법을 일반화(generalization of linear interpolation)한 것입니다. 선형 보간법에서는 선형 함수를 사용했다면, 다항식 보간법에서는 더 높은 차수의 다항식 함수를 사용해서 보간하는 것으로 대체한 것입니다. 

일반적으로, 만약 우리가 n개의 데이터 포인트를 가지고 있다면 모든 데이터 포인트를 통과하는 n-1 차수의 다차항 함수가 존재합니다. 보간 오차는 데이터 포인트 간의 거리의 n 차승에 비례(interpolation error is proportional to the distance between the data points to the power n)하며, 다차항 함수는 미분가능합니다. 따라서 선형 보간법의 대부분의 문제를 다항식 보간법은 극복합니다. 하지만 다항식 보간법은 선형 보간법에 비해 복잡하고 연산에 많은 비용이 소요됩니다. 그리고 끝 점(end point) 에서는 진동하면서 변동성이 큰 값을 추정하는 문제가 있습니다. 

 

스플라인 보간법은 각 데이터 포인트 구간별로 낮은 수준의 다항식 보간을 사용 (Spline interpolation uses low-degree polynomials in each of the intervals) 합니다. 그리고 이들이 함께 부드럽게 연결되어서 적합될 수 있도록 다항식 항목을 선택(, and chooses the polynomial pieces such that they fit smoothly together)합니다. 이렇게 적합된 함수를 스플라인(Spline) 이라고 합니다. 

 

스플라인 보간법(Spline Interpolation)은 다항식 보간법의 장점은 살리고 단점은 피해간 보간법입니다. 스플라인 보간법은 다항식 보간법처럼 선형 보간법보다 보간 오차가 더 작은 반면에, 고차항의 다항식 보간법보다는 보간 함수가 부드럽고 평가하기가 쉽습니다.  

 

## Spline Interpolation
f_quadr = interpolate.interp1d(
    x, y, kind='quadratic') # cubic

y_new_quadr = f_quadr(xnew)

plt.plot(x, y, "o", xnew, y_new_quadr)
plt.show()

Polynomial interpolation

 

 

[ Reference ]

1. 보간법(interpolation): https://en.wikipedia.org/wiki/Interpolation

2. scipy 모듈: https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html

 

 

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

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

 

728x90
반응형
Posted by Rfriend
,

이전 포스팅에서는 무작위(확률, 임의) 표본 추출과 관련하여,

- numpy.random() 메소드를 이용하여 확률분포별 확률 표본 추출, 난수 생성: https://rfriend.tistory.com/284

- 그룹별 무작위 표본 추출: https://rfriend.tistory.com/407

- 기계학습을 위한 Train, Test 데이터셋 분할: https://rfriend.tistory.com/519

- 층화 무작위 추출을 통한 Train, Test 데이터셋 분할: https://rfriend.tistory.com/520

방법에 대하여 소개하였습니다.



이번 포스팅에서는 Python pandas 모듈의 DataFrame.sample() 메소드를 사용해서 DataFrame으로 부터 무작위 (확률, 임의) 표본 추출 (random sampling) 하는 방법을 소개하겠습니다.


(1) DataFrame으로 부터 특정 개수의 표본을 무작위로 추출하기 (number)

(2) DataFrame으로 부터 특정 비율의 표본을 무작위로 추출하기 (fraction)

(3) DataFrame으로 부터 복원 무작위 표본 추출하기 (random sampling with replacement)

(4) DataFrame으로 부터 가중치를 부여하여 표본 추출하기 (weights)

(5) DataFrame으로 부터 칼럼에 대해 무작위 표본 추출하기 (axis=1, axis='column)

(6) DataFrame으로 부터 특정 칼럼에 대해 무작위 표본 추출한 결과를 numpy array로 할당하기



[ pandas DataFrame에서 무작위 (확률) 표본 추출하기: pandas.DataFrame.sample() ]



  (1) DataFrame으로 부터 특정 개수의 표본을 무작위(확률)로 추출하기 (number)


예제로 사용할 4개의 관측치와 3개의 칼럼을 가진 pandas DataFrame을 만들어보겠습니다.

(참조 [1] 의 pandas tutorial 코드 사용하였습니다.)



import pandas as pd

df = pd.DataFrame({'num_legs': [2, 4, 8, 0],
                   'num_wings': [2, 0, 0, 0],
                   'num_specimen_seen': [10, 2, 1, 8]},
                  index=['falcon', 'dog', 'spider', 'fish'])

df


num_legsnum_wingsnum_specimen_seen
falcon2210
dog402
spider801
fish008

 



DataFrame.sample() 메소드의 n 매개변수를 사용해서 특정 개수 (number)의 표본을 무작위로 추출할 수 있습니다. 그리고 random_state 매개변수는 무작위(확률) 표본 추출을 위한 난수(random number)를 생성할 때 초기값(seed number) 로서, 재현가능성(reproducibility)을 위해서 설정해줍니다.


아래 예에서는 총 4개 관측치 중에서 2개의 관측치 (n=2) 를 무작위 표본 추출해보았습니다. Index를 기준으로 n 개수 만큼 표본을 추출해서 모든 칼럼의 값을 pandas DataFrame 자료구조로 반환합니다.



df.sample(n=2, # number of items from axis to return.
          random_state=1004) # seed for random number generator for reproducibility



num_legsnum_wingsnum_specimen_seen
falcon2210
fish008

 




  (2) DataFrame으로 부터 특정 비율의 표본을 무작위로 추출하기 (fraction)


DataFrame으로 부터 특정 비율(fraction)으로 무작위 표본 추출을 하고 싶으면 frac 매개변수에 0~1 사이의 부동소수형(float) 값을 입력해주면 됩니다.



df.sample(frac=0.5, # fraction of axis items to return.
          random_state=1004)



num_legsnum_wingsnum_specimen_seen
falcon2210
fish008

 



만약 비복원 추출 모드 (replace = False, 기본 설정) 에서 frac 값이 1을 초과할 경우에는 "ValueError: Replace has to be set to 'True' when upsampling the population 'frac' > 1." 이라는 에러가 발생합니다. 왜냐하면 모집단의 표본 개수 (100%, frac=1) 보다 더 많은 표본을 비복원 추출로는 할 수 없기 때문입니다. (복원 추출의 경우 동일한 관측치를 다시 표본 추출할 수 있으므로 frac > 1 인 경우도 가능함.)



## ValueError: Replace has to be set to `True` when upsampling the population `frac` > 1.
df.sample(frac=1.5,
          random_state=1004)


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-45-2fcc4494d7ae> in <module>
----> 1 df.sample(frac=1.5, # fraction of axis items to return. 
      2           random_state=1004)

~/opt/anaconda3/lib/python3.8/site-packages/pandas/core/generic.py in sample(self, n, frac, replace, weights, random_state, axis)
   5326             n = 1
   5327         elif frac is not None and frac > 1 and not replace:
-> 5328             raise ValueError(
   5329                 "Replace has to be set to `True` when "
   5330                 "upsampling the population `frac` > 1."

ValueError: Replace has to be set to `True` when upsampling the population `frac` > 1.

 



만약 DataFrame.sample() 메소드에서 표본 개수 n 과 표본추출 비율 frac 을 동시에 설정하게 되면 "ValueError: Please enter a value for 'frac' OR 'n', not both" 에러가 발생합니다. n 과 frac 둘 중에 하나만 입력해야 합니다.



## parameter 'n' and 'frac' cannot be used at the same time.
## ValueError: Please enter a value for `frac` OR `n`, not both
df.sample(n=2, frac=0.5)


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-6-b31ebc150882> in <module>
      1 ## parameter 'n' and 'frac' cannot be used at the same time.
      2 ## ValueError: Please enter a value for `frac` OR `n`, not both
----> 3 df.sample(n=2, frac=0.5)

~/opt/anaconda3/lib/python3.8/site-packages/pandas/core/generic.py in sample(self, n, frac, replace, weights, random_state, axis)
   5335             n = int(round(frac * axis_length))
   5336         elif n is not None and frac is not None:
-> 5337             raise ValueError("Please enter a value for `frac` OR `n`, not both")
   5338 
   5339         # Check for negative sizes

ValueError: Please enter a value for `frac` OR `n`, not both

 




  (3) DataFrame으로 부터 복원 무작위 표본 추출하기

      (random sampling with replacement)


한번 추출한 표본을 다시 모집단에 되돌려 넣고 추출하는 방법을 복원 추출법 (sampling with replacement) 이라고 합니다. 복원 추출법을 사용하면 동일한 표본이 중복해서 나올 수 있습니다.


DataFrame.sample() 메소드에서는 repalce=True 로 설정하면 복원 추출을 할 수 있습니다. 많은 경우 한번 추출된 표본은 되돌려 놓지 않고 표본을 추출하는 비복원 추출(sampling without replacement)을 사용하며, 기본 설정은 replace=False 입니다.



## replace=True: random sampling with replacement
df.sample(n=8, # or equivalently: frac=2
          replace=True, # random sampling with replacement
          random_state=1004)



num_legsnum_wingsnum_specimen_seen
spider801
fish008
fish008
dog402
fish008
fish008
fish008
spider801

 



만약 비복원 추출 모드 (replace=False) 에서 원본 DataFrame 의 관측치 개수 (행의 개수) 보다 많은 수의 표본을 무작위 추출하고자 한다면 "ValueError: Cannot take a larger sample than population when 'replace=False'" 에러 메시지가 발생합니다.  모집단이 가지고 있는 관측치 수보다 더 많은 수의 표본을 중복이 없는 "비복원 추출"로는 불가능하기 때문입니다.

(복원추출(sampling with replacement, replace=True) 모드 에서는 동일한 표본을 중복 추출이 가능하므로 모집단 관측치 수보다 많은 수의 표본 추출이 가능함.)



## ValueError: Cannot take a larger sample than population when 'replace=False'
df.sample(n=8,
          replace=False # random sampling without replacement
)


---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-42-40c76bd4c271> in <module>
      1 ## replace=True: random sampling with replacement
----> 2 df.sample(n=8, # or equivalently: frac=2
      3           replace=False # random sampling without replacement
      4 )

~/opt/anaconda3/lib/python3.8/site-packages/pandas/core/generic.py in sample(self, n, frac, replace, weights, random_state, axis)
   5343             )
   5344 
-> 5345         locs = rs.choice(axis_length, size=n, replace=replace, p=weights)
   5346         return self.take(locs, axis=axis)
   5347 

mtrand.pyx in numpy.random.mtrand.RandomState.choice()

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

 




  (4) DataFrame으로 부터 가중치를 부여하여 표본 추출하기 (weights)


만약에 DataFrame 내의 특정 칼럼의 값을 기준으로 가중치를 부여하여 무작위 표본 추출을 하고 싶다면 DataFrame.sample() 메소드의 weights 매개변수에 가중치로 사용할 칼럼 이름을 설정해주면 됩니다.


아래 예에서는 df DataFrame의 'num_specimen_seen' 칼럼의 값이 크면 클수록 표본으로 뽑힐 확률이 더 크도록 가중치(weights)를 부여해보았습니다. 아니나 다를까, 'num_specimen_seen' 값이 10, 8 인 falcon, fish가 표본으로 추출이 되었네요. 

(물론, 표본추출 시행을 계속 하다보면 num_specimen_seen 값이 1인 spider나 2인 dog 도 표본으로 뽑히는 때가 오긴 올겁니다. 다만, num_specimen_seen 값의 가중치로 인해 표본 추출될 확률이 낮아 상대적으로 작은 빈도로 추출이 되겠지요.)



## Using a DataFrame column as weights.
## Rows with larger value in the num_specimen_seen column are more likely to be sampled.
df.sample(n=2,
          weights='num_specimen_seen'

)



num_legsnum_wingsnum_specimen_seen
falcon2210
fish008

 




  (5) DataFrame으로 부터 칼럼에 대해 무작위 표본 추출하기 (axis=1, axis='column)


위의 (1) ~ (4) 까지는 axis=0, 즉 Index 에 대해서 무작위 표본 추출을 해서 전체 칼럼의 값을 반환하였습니다.


DataFrame.sample() 메소드의 axis 매개변수를 axis=1, 또는 axis='column' 으로 설정을 해주면 여러개의 칼럼에 대해서 무작위로 표본 추출을 해서 전체 행(all rows, random sampled columns) 을 반환합니다. (이런 요건의 분석은 그리 많지는 않을것 같습니다만, 이런 기능도 있다는 정도로만 알아두면 되겠습니다.)



## Axis to sample: by column
df.sample(n=2,
          random_state=1004,
          axis=1) # or equivalently, axis='column'



num_legsnum_wings
falcon22
dog40
spider80
fish00

 



axis 매개변수의 기본 설정은 대부분의 분석 요건에 해당하는 Index 기준의 무작위 표본 추출인 axis=0 (or, axis='index') 입니다.



## Axis to sample: by index
df.sample(n=2,
          random_state=1004,
          axis=0) # or equivalently, axis='index', default



num_legsnum_wingsnum_specimen_seen
falcon2210
fish008

 




  (6) DataFrame으로 부터 특정 칼럼에 대해 무작위 표본 추출한 결과를

       numpy array로 할당하기


만약 DataFrame의 여러개의 칼럼 중에서 특정 하나의 칼럼에 대해서만 무작위 표본 추출을 하고 싶다면 DataFrame['column_name'] 형식으로 먼저 Series 로 특정 칼럼의 값을 가져오고, 이에 대해서 sample() 메소드를 사용하면 됩니다.



## Sampling only for a column
df['num_legs'].sample(n=2, random_state=1004)


[Out] 
falcon 2 fish 0 Name: num_legs, dtype: int64

 



df['num_specimen_seen'].sample(n=2, random_state=1004)


[Out]
falcon 10 fish 8 Name: num_specimen_seen, dtype: int64

 



이렇게 DataFrame으로 부터 특정 하나의 칼럼 값을 Series 로 인덱싱해와서 무작위 표본 추출을 하면, 역시 그 결과 객체의 데이터 유형도 Series 입니다.



## Assigning sampling results as Series
samp_Series = df['num_legs'].sample(n=2)
type(samp_Series)


[Out] pandas.core.series.Series

 



만약, DataFrame으로 부터 특정 하나의 칼럼 값 Series 로 부터의 무작위 표본 추출 결과를 Numpy Array로 할당해서 결과를 가져오고 싶다면 numpy.array() 로 Series 를 array 로 변환해주면 됩니다.



## Assigning sampling results as numpy array
import numpy as np
samp_array = np.array(df['num_legs'].sample(n=2))
type(samp_array)

[Out] numpy.ndarray


samp_array

[Out] array([0, 2])




[ Reference ]

* pandas.DataFrame.sample: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.sample.html



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

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




728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 pandas 모듈의 DataFrame.iterrows(),  DataFrame.iteritems(), DataFrame.itertuples() 의 메소드 3총사와 for loop 반복문 활용하여 pandas DataFrame 자료의 행, 열, (행, 열) 튜플에 대해서 순환 반복 (for loop iteration) 하여 자료를 반환하는 방법을 소개하겠습니다.


(1) pd.DataFrame.iterrows() : 행에 대해 순환 반복
    (Iterate over DataFrame rows as (index, Series) pairs.)

(2) pd.DataFrame.iteritems() : 열에 대해 순환 반복
    (Iterate over DataFrame (column name, Series) pairs.)

(3) pd.DataFrame.itertuples() : 이름이 있는 튜플 (인덱스, 행, 열) 에 대해 순환 반복

    (Iterate over DataFrame rows as namedtuples)



[ Pandas DataFrame의 행, 열, (행, 열) 튜플 순환 반복 ]





  (1) DataFrame.iterrows() : 행에 대해 순환 반복
      (Iterate over DataFrame rows as (index, Series) pairs.)


먼저 pandas 모듈을 importing 하고, 예제로 사용할 2개의 칼럼과 인덱스를 가진 간단한 DataFrame을 만들어보겠습니다.



import pandas as pd


df = pd.DataFrame(
    {'price': [100, 200, 300],
     'weight': [20.3, 15.1, 25.9]},
    index=['idx_a', 'idx_b', 'idx_c'])

df


priceweight
idx_a10020.3
idx_b20015.1
idx_c30025.9




이제 DataFrame.iterrows() 메소드와 for loop 반복문을 사용해서 행(row)에 대해서 순환하면서 인덱스 이름과 각 행별 칼럼별 데이터를 출력해보겠습니다.



## DataFrame.iterrows()
for idx, row in df.iterrows():
    print("** index name:", idx)
    print(row)
    print("------"*5)


[Out]
** index name: idx_a price 100.0 weight 20.3 Name: idx_a, dtype: float64 ------------------------------ ** index name: idx_b price 200.0 weight 15.1 Name: idx_b, dtype: float64 ------------------------------ ** index name: idx_c price 300.0 weight 25.9 Name: idx_c, dtype: float64 ------------------------------



DataFrame에 여러개의 칼럼이 있고, 이중에서 특정 칼럼에 대해서만 행을 순회하면서 행별 특정 칼럼의 값을 반복해서 출력하고 싶으면 row['column_name'] 또는 row[position_int] 형식으로 특정 칼럼의 이름이나 위치 정수를 넣어주면 됩니다.



## accessing to column of each rows by indexing
for idx, row in df.iterrows():
    print(idx)
    print(row['price']) # or print(row[0])
    print("-----")


[Out]
idx_a 100.0 ----- idx_b 200.0 ----- idx_c 300.0 -----



DataFrame.iterrows() 메소드는 결과물로 (index, Series) 짝(pairs)을 반환합니다. 따라서 원본 DataFrame에서의 데이터 유형일 보존하지 못하므로 행별 Series 에서는 데이터 유형이 달라질 수 있습니다.


가령, 예제의 DataFrame에서 'price' 칼럼의 데이터 유형은 '정수형(integer64)' 인데 반해, df.iterrows() 로 반환된 'row['price']'의 데이터 유형은 '부동소수형(float64)'으로 바뀌었습니다.



## DataFrame.iterrows() returns a Series for each row,
## it does not preserve dtypes across the rows.
print('Data type of df price:', df['price'].dtype) # int
print('Data type of row price:', row['price'].dtype) # float


[Out]
Data type of df price: int64 Data type of row price: float64





  (2) DataFrame.iteritems() : 열에 대해 순환 반복
      (Iterate over DataFrame (column name, Series) pairs.)


위의 (1)번이 DataFrame의 행(row)에 대해 순환 반복을 했다면, 이번에는 pandas DataFrame의 열(column)에 대해 iteritems() 메소드와 for loop 문을 사용해 순환 반복(iteration) 하면서 '칼럼 이름 (column name)' 과 '행별 값 (Series for each row)' 을 짝으로 하여 출력해 보겠습니다.



df


priceweight
idx_a10020.3
idx_b20015.1
idx_c30025.9



for col, item in df.iteritems():
    print("** column name:", col)
    print(item) # = print(item, sep='\n')
    print("-----"*5)


[Out]
** column name: price idx_a 100 idx_b 200 idx_c 300 Name: price, dtype: int64 ------------------------- ** column name: weight idx_a 20.3 idx_b 15.1 idx_c 25.9 Name: weight, dtype: float64 -------------------------




만약 DataFrame.iteritems() 와 for loop 문으로 열(column)에 대해 순환 반복하여 각 행(row)의 값을 출력하는 중에 특정 행만을 출력하고 싶으면 '행의 위치 정수(position index of row)'나 '행의 인덱스 이름 (index name of row)' 으로 item 에서 인덱싱해주면 됩니다.



for col, item in df.iteritems():
    print(col)
    print(item[0]) # = print(item['idx_a'])


[Out]
price 100 weight 20.3





  (3) DataFrame.itertuples() : 이름이 있는 튜플 (인덱스, 행, 열) 에 대해 순환 반복

    (Iterate over DataFrame rows as namedtuples)


위의 (1) 번의 DataFrame.iterrows() 에서는 DataFrame의 행(row)에 대해 순환 반복, (2) 번의 DataFrame.iteritems() 에서는 열(column, item)에 대해 순환 반복하였습니다. 반면에, 경우에 따라서는 (인덱스, 행, 열) 의 튜플 묶음 단위로 순환 반복을 하고 싶을 때 DataFrame.itertuples() 메소드를 사용할 수 있습니다.


각 행과 열에 대해서 순환 반복하면서 값을 가져오고, 이를 zip() 해서 묶어주는 번거로운 일을 DataFrame.itertuples() 메소드는 한번에 해주니 알아두면 매우 편리한 메소드입니다.


아래의 예는 DataFrame.itertuples() 메소드와 for loop 문을 사용해서 'df' DataFrame의 이름있는 튜플인 namedtuple (Index, row, column) 에 대해서 순환 반복하면서 출력을 해보겠습니다.



df


priceweight
idx_a10020.3
idx_b20015.1
idx_c30025.9



for row in df.itertuples():
    print(row)


[Out] 
Pandas(Index='idx_a', price=100, weight=20.3) Pandas(Index='idx_b', price=200, weight=15.1) Pandas(Index='idx_c', price=300, weight=25.9)



만약 인덱스를 포함하고 싶지 않다면 index=False 로 매개변수를 설정해주면 됩니다.



## By setting the indx=False, we can remove the index as the first element of the tuple.
for row in df.itertuples(index=False):
    print(row)


[Out] 
Pandas(price=100, weight=20.3) Pandas(price=200, weight=15.1) Pandas(price=300, weight=25.9)



DataFrame.itertuples() 메소드가 이름있는 튜플(namedtuples)을 반환한다고 했는데요, name 매개변수로 튜플의 이름을 부여할 수도 있습니다. 아래 예에서는 name='Product' 로 해서 튜플에 'Product'라는 이름을 부여해보았습니다.



## Setting a custom name for the yielded namedtuples.
for row in df.itertuples(name='Product'):
    print(row)


[Out]
Product(Index='idx_a', price=100, weight=20.3) Product(Index='idx_b', price=200, weight=15.1) Product(Index='idx_c', price=300, weight=25.9)



DataFrame.iterrows() 는 (index, Series) 짝을 반환하다보니 원본 DataFrame의 데이터 유형을 보존하지 못한다고 했는데요, DataFrame.itertuples() 는 원본 DataFrame의 데이터 유형을 그대로 보존합니다.


아래 예에서 볼 수 있듯이 df['price']의 데이터 유형과 df.itertuples()의 결과의 row.price 의 데이터 유형이 둘 다 '정수(int64)'로 동일합니다.



## DataFrame.itertuples() preserves dtypes, returning namedtuples of the values.
print('Data type of df price:', df['price'].dtype) # int
print('Data type of row price:', type(row.price)) # int


[Out] 
Data type of df price: int64 Data type of row price: <class 'int'>



[Reference]

* DataFrame.iterrows(): https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.iterrows.html#pandas.DataFrame.iterrows

* DataFrame.iteritems(): https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.iteritems.html

* DataFrame.itertuples(): https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.itertuples.html#pandas.DataFrame.itertuples


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

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




728x90
반응형
Posted by Rfriend
,

ZIP 파일 포맷은 일반적으로 자료를 압축하여 보관하는 표준 포맷입니다. 대용량의 데이터를 압축하는 것은 데이터 저장 공간을 적게 사용하고, 데이터 전송에 있어서도 성능 향상을 기대할 수 있으며, 하나의 압축된 파일로 관리할 수 있어 편리합니다.


Python의 zipfile 모듈은 ZIP 파일을 생성하고, 읽기, 쓰기, 추가하기, 압축 파일 해제하기, 압축 파일 리스트와 정보 보기 등을 할 수 있는 클래스를 제공합니다.


이번 포스팅에서는 Python의 zipfile 모듈을 사용해서 (Python 3.x 버전 기준)


(1) 압축 ZIP 파일 쓰기 (write)

(2) 압축 ZIP 파일 읽기 (read)

(3) 압축 ZIP 파일 이름(filename), 자료 리스트(namelist()), 파일 정보(getinfo) 보기

(4) 압축 ZIP 파일 해제하기 (extract)

(5) 웹에서 압축 ZIP 파일 다운로드하여 압축 해제하기 (download and extract)



[ Python zipfile 모듈로 압축 파일 쓰기, 읽기, 해제하기 ]



  (1) 압축 ZIP 파일 쓰기 (write)


먼저, Python으로 (a) 압축 ZIP 파일을 다루는 zipfile 모듈과, (b) 경로(directory, path) 및 폴더/파일을 관리를 할 수 있게 해주는 os 모듈을 importing 하겠습니다.

(cf. Python의 os 모듈을 사용해서 경로 및 폴더/파일 관리하는 방법은 https://rfriend.tistory.com/429 포스팅을 참고하세요.)


다음으로, os 모듈의 chdir() 함수를 사용해서 "Downloads" 폴더로 작업 경로를 변경하겠습니다.

os.getcwd() 로 현재 작업 경로를 확인해보니 "Downloads" 폴더로 작업 경로가 잘 변경되었네요.

os.listdir() 은 현재 작업 경로에 들어있는 파일 리스트를 반환합니다. ['sample_1.txt', 'sample_2.txt', 'sample_3.txt'] 의 3개 텍스트 파일이 예제로 들어있습니다.



import zipfile
import os


## change working directory
base_dir = '/Users/ihongdon/Downloads'
os.chdir(base_dir)

## check the current working directory
os.getcwd()

[Out] '/Users/ihongdon/Downloads'


## show the lists of files in the current working directory
os.listdir()

['sample_2.txt', 'sample_3.txt', 'sample_1.txt']




(1-1) mode='w' : 새로운 압축 파일 쓰기 (단, 기존 압축 파일 있으면 덮어쓰기)


zipfile.ZipFile(file, mode='r') 에서 mode 에는 'w', 'x', 'a', 'r'의 4개 모드가 있고, 기본 설정값은 'r' (읽기) 입니다. 이들 4개 모드별 기능은 아래와 같습니다.


[ zipfile.ZipFile(file, mode) 에서 mode='w'/'x'/'a'/'r' 별 기능 ]

  • mode='w': 새로운 ZIP 압축 파일을 쓰기 (단, 기존 압축 파일이 있으면 덮어쓰기)
                   (to truncate and write a new file)
  • mode='x': 새로운 ZIP 압축 파일을 쓰기 (단, 기존 압축 파일이 있으면 FileExistsError 발생)
                   (to exclusively create and write a new file)
  • mode='a': 기존 ZIP 압축 파일에 자료 추가하기 (to append additional files to an existing ZIP file)
  • mode='r': 기존 ZIP 압축 파일의 자료 읽기 (to read an existing file). 기본 설정 값


myzip_w = zipfile.ZipFile('sample.zip', 'w') 로 'myzip_w'라는 이름의 ZipFile 객체를 새로 만들어 주고, myzip_w.write('sample_1.txt') 함수로 'sample.zip'의 ZIP 파일에 'sample_1.txt' 텍스트 파일을 압축해서 써줍니다.


ZIP 파일을 열고나서 작업 (쓰기, 추가하기, 읽기 등)이 다 끝났으면 시스템이나 프로그램을 종료하기 전에 ZipFile.close() 메소드를 써서 작업 중인 ZIP 파일을 닫아주어야 합니다. 만약 close() 를 하지 않은 상태에서 프로그램을 종료하면 ZIP 파일에 정상적으로 자료가 기록이 되지 않을 것입니다.


ZipFile.is_zipfile(file) 메소드는 ZIP 파일이 존재하면 TRUE를 반환하고, 존재하지 않으면 FALSE를 반환합니다.



## (1) mode='w': to truncate and write a new file
myzip_w = zipfile.ZipFile('sample.zip', 'w')
myzip_w.write('sample_1.txt')

## You must call close() before exiting your program,
## or essential records will not be written.
myzip_w.close()


## ZipFile.is_zipfile(): Return True if a valid ZIP file exists.
zipfile.is_zipfile('sample.zip')

[Out] True




ZipFile 객체는 맥락 관리자(context manager) 이므로 'with 문 (with statement)' 을 지원합니다. 따라서 위의 (1-1) 예제 코드를 아래처럼 with 문을 사용해서 ZIP 파일 쓰기를 할 수도 있습니다.



with zipfile.ZipFile('sample.zip', 'w') as myzip:
    myzip.write('sample_1.txt')
    myzip.close()

 




(1-2) mode='x' : 새로운 압축 파일 쓰기 (단, 기존 파일 있으면 FileExistsError 발생)


위의 mode='w'와는 달리, mode='x'는 새로운 압축 파일을 생성할 때 만약 같은 이름의 ZIP 파일이 존재한다면 'FileExistsError' 가 발생한다는 점이 다릅니다. (to exclusively create and write a new file.)


위의 (1-1)번 예에서 'sample.zip' 이름의 ZIP 파일을 이미 만들었습니다. 여기에 zipfile.ZipFile('sample.zip', mode='x') 로 해서 'sample.zip' 파일 이름으로 ZIP 압축 파일을 만들려고 하면 아래처럼 'FileExistsError: [Errno 17] File exists: 'sample.zip' 의 에러가 발생합니다.



## (2) mode='x': to exclusively create and write a new file.
## if file refers to an existing file, a 'FileExistsError' will be raised.
myzip_x = zipfile.ZipFile('sample.zip', 'x')

[Out]
--------------------------------------------------------------------------- FileExistsError Traceback (most recent call last) <ipython-input-7-bd84b411165c> in <module> 1 ## (2) mode='x': to exclusively create and write a new file. 2 ## if file refers to an existing file, a 'FileExistsError' will be raised. ----> 3 myzip_x = zipfile.ZipFile('sample.zip', 'x') ~/opt/anaconda3/lib/python3.8/zipfile.py in __init__(self, file, mode, compression, allowZip64, compresslevel, strict_timestamps) 1249 while True: 1250 try: -> 1251 self.fp = io.open(file, filemode) 1252 except OSError: 1253 if filemode in modeDict: FileExistsError: [Errno 17] File exists: 'sample.zip'

 



위의 'FileExistsError'가 발생했다면, 아래처럼 ZIP 파일 이름을 기존에는 없는 파일 이름으로 바꾸어서 zipfile.ZipFile(new_file_name, mode='x') 로 해서 압축 파일을 생성할 수 있습니다.

(mode='w' 로 하면 기존 파일을 덮어쓰기 하므로 주의가 필요합니다.)


ZipFile.namelist() 는 ZipFile 객체에 압축되어 있는 자료(archives)의 이름 리스트를 출력해줍니다.



myzip_x = zipfile.ZipFile('sample2.zip', 'x')
myzip_x.write('sample_2.txt')
myzip_x.close()

myzip_x.namelist()

[Out] ['sample_2.txt']





(1-3) mode='a' : 기존 ZIP 압축 파일에 자료 추가 (to append, add up)


만약 기존에 존재하는 ZIP 파일에 새로운 자료를 추가(append)하고 싶다면 mode='a' 로 설정해주면 됩니다.


아래 예제에서는 위의 (1-1)에서 'sample_1.txt'의 텍스트 파일을 'sample.zip' 이름으로 압축해서 이미 만들어두었던 ZIP 파일에 더하여, 'sample_2.txt', 'sample_3.txt' 의 텍스트 파일까지 추가하여 'sample.zip' 이름의 ZIP 파일에 압축해보겠습니다.



## (3) mode='a': to append to an existing file.
myzip_a = zipfile.ZipFile('sample.zip', 'a')
myzip_a.write('sample_2.txt')
myzip_a.write('sample_3.txt')
myzip_a.close()

myzip_a.namelist()

[Out] ['sample_1.txt', 'sample_2.txt', 'sample_3.txt']





(1-4) 여러개의 자료를 하나의 압축 ZIP 파일에 쓰기 (for loop, ZipFile(), write())


하나의 ZIP 압축 파일에 여러개의 자료를 압축해서 쓰고 싶을 때는 for loop 반복문을 같이 사용해주면 됩니다. (mode 는 필요와 상황에 맞게 'w', 'x', 'a' 중에서 선택)


아래 예제는 'myzip_all' 이름의 ZipFile 객체로 'sample_all.zip' 의 ZIP 파일에 ['sample_1.txt', 'sample_2.txt', 'sample_3.txt'] 의 3개 텍스트 파일들을 for loop 반복문을 사용해서 하나씩 차례대로 호출해서 myzip_all.write(f) 로 'sample_all.zip' 파일에 써주었습니다.



## (4) writing files to a zip file: with statement & for loop
file_list = ['sample_1.txt', 'sample_2.txt', 'sample_3.txt']

with zipfile.ZipFile('sample_all.zip', 'w') as myzip_all:
    for f in file_list:
        myzip_all.write(f)
        print(f, 'is written to myzip_all.zip')
    myzip_all.close()


[Out]
sample_1.txt is written to myzip_all.zip sample_2.txt is written to myzip_all.zip sample_3.txt is written to myzip_all.zip


myzip_all.namelist()

[Out] ['sample_1.txt', 'sample_2.txt', 'sample_3.txt']





(1-5) zipfile.ZipFile(file, mode='r',

           compression=ZIP_STORED, allowZip64=True, compresslevel=None)


매개변수

설명

 compression

 compression은 자료를 압축 파일에 쓰기 위한 ZIP 압축 메소드이며, 기본 설정값은 ZIP_STORED 입니다.


Python 버전 3.1 부터 아래의 파일과 같은 객체를 지원합니다.

  • zipfile.ZIP_STORED  (* default)
  • zipfile.ZIP_DEFLATED
  • zipfile.ZIP_BZIP2

Python 버전 3.3 부터는 ZIP_LZMA 객체를 지원합니다.

  • zipfile.ZIP_LZMA

 allowZip64

 allowZip64=True (기본 설정) 이면 ZIP 파일 크기가 4GB를 넘을 경우 ZIP64 extensions 를 사용해서 ZIP 파일을 생성합니다.

 

 만약 allowZip64=False 설정인데 ZIP 파일 크기가 4GB를 넘을 경우에는 exception error 가 발생합니다.

 compresslevel

 compresslevel 매개변수는 자료를 압축할 수준을 지정할 때 사용합니다.

(compression 이 ZIP_STORED, ZIP_LZMA 일 경우에는 효과가 없으며, ZIP_DEPLATED, ZIP_BZIP2 에만 설정 가능합니다.)

  • compression=ZIP_DEFLATED 일 경우 compresslevel=0~9 까지 설정 가능
  • compression=ZIP_BZIP2 일 경우 compresslevel=1~9 까지 설정 가능




  (2) 압축 ZIP 파일 읽기 (read)


ZIP 압축 파일에 들어있는 자료를 읽으려면 zipfile.ZipFile(file, mode='r') 로 해서 ZipFile 객체를 '읽기 모드'로 생성한 후, ZipFile.read() 메소드로 ZIP 파일 내 압축되어 있는 자료를 읽을 수 있습니다.

아래 예제는 위의 (1-1)에서 만들었던 'sample.zip'의 ZIP 파일 안에 압축되어 있는 'sample_1.txt' 텍스트 자료를 읽어본 것입니다. 압축을 해제하지 않고도 ZIP 압축 파일 내의 특정 자료를 선택해서 그 자료만 읽을 수 있어서 편리합니다.


## sample.zip
myzip_w.namelist()

[Out] ['sample_1.txt']


## mode='r': to read an existing file
myzip_r = zipfile.ZipFile('sample.zip', 'r')
print(myzip_r.read('sample_1.txt'))

[Out] b'x1,x2,x3\n1,2,3\n4,5,6\n7,8,9\n'


# ## or equivalently above
# with myzip_r.open('sample_1.txt') as s1:
#     print(s1.read())




위의 압축 파일 내 자료를 읽은 결과가 눈에 잘 안들어 올 수도 있는데요, 아래에는 참고로 pandas 의 read_csv() 메소드를 사용해서 'sample_1.txt' 파일을 출력해본 것입니다.


import pandas as pd

sample_1_df = pd.read_csv('sample_1.txt')
print(sample_1_df)

[Out]
x1 x2 x3 0 1 2 3 1 4 5 6 2 7 8 9





  (3) 압축 ZIP 파일 이름(filename), 자료 리스트(namelist()), 파일 정보(getinfo) 보기


(3-1) ZipFile.is_zipfile(file) : Zip 파일이 존재하면 True, 존재하지 않으면 False



file_list = ['sample_1.txt', 'sample_2.txt', 'sample_3.txt']

with zipfile.ZipFile('sample_all.zip', 'w') as myzip_all:
    for f in file_list:
        myzip_all.write(f)
    myzip_all.close()


## ZipFile.is_zipfile(): Return True if a valid ZIP file exists.
zipfile.is_zipfile('sample_all.zip')

[Out] True

 



(3-2) ZipFile.filename : ZIP 압축 파일 이름 출력



## ZipFile.filename: Name of the ZIP file
myzip_all.filename

[Out] 'sample_all.zip'




(3-3) ZipFile.namelist() : ZIP 압축 파일 내 자료 이름 리스트 출력



## file name lists of sample_all.zip
myzip_all.namelist()

[Out] ['sample_1.txt', 'sample_2.txt', 'sample_3.txt']




(3-4) ZipFile.getinfo(member) : ZIP 파일 내 자료(member)의 정보 출력


파일 이름 (file name), 파일 모드 (file mode), 파일 크기 (file size)



## ZipFile.getinfo(): Zip information about the archive member name.
myzip_all.getinfo('sample_1.txt')

[Out] <ZipInfo filename='sample_1.txt' filemode='-rw-r--r--' file_size=27>




  (4) 압축 ZIP 파일 해제하기 (extract)


(4-1) ZipFile.extract(file, path) : ZIP 파일 내 1개의 자료만 압축 해제하기


이때 압축을 해제한 자료를 저장할 경로(path)를 별도로 지정해 줄 수 있습니다. (path 를 지정하지 않으면 현재 작업경로에 압축 해제함)



## (4-1) ZipFile.extract()
## : extracting a member from the archive to the current working directory.
extract_path = '/Users/ihongdon/Downloads/sample_3'
zipfile.ZipFile('sample_all.zip').extract('sample_3.txt', path=extract_path)

[Out] '/Users/ihongdon/Downloads/sample_3/sample_3.txt'

 



위의 (4-1)에서는 압축 해제한 1개 파일을 저장할 경로(path)를 지정해주었는데요, 해당 경로에 os.listdir(extract_path) 로 확인해 보니 원하는 'sample_3.txt' 텍스트 자료가 잘 압축 해제되어 저장되어 있네요.



os.listdir(extract_path)

[Out] ['sample_3.txt']

 



(4-2) ZipFile.extractall() : ZIP 파일 내 모든 자료를 압축 해제



## (4-2) ZipFile.extractall()
## : extracting all members from the archive to the current working directory.
extractall_path = '/Users/ihongdon/Downloads/sample_all'
zipfile.ZipFile('sample_all.zip').extractall(extractall_path)


os.listdir(extractall_path)

[Out] ['sample_2.txt', 'sample_3.txt', 'sample_1.txt']





  (5) 웹에서 ZIP 파일 다운로드하여 압축 해제하기 (download and extract ZIP file)


아래 예제는 웹사이트에서 영화 추천에 사용되는 영화 평가 점수(movie ratings)를 모아놓은  데이터셋('movielens.csv', etc.)ZIP 포맷으로 압축해 놓은 'ml-latest-small.zip' 파일을 Keras의 메소드를 사용해 다운로드 한 다음에, zipfile 모듈의 ZipFile.extractall() 메소드로 전체 자료를 압축 해제한 것입니다.



## Download the movielens data from website url
import tensorflow.keras as keras
from zipfile import ZipFile
from pathlib import Path

import os


movielens_data_file_url = (
    "http://files.grouplens.org/datasets/movielens/ml-latest-small.zip"
)

movielens_zipped_file = keras.utils.get_file(
    "ml-latest-small.zip", movielens_data_file_url, extract=False
)

keras_datasets_path = Path(movielens_zipped_file).parents[0]
movielens_dir = keras_datasets_path / "ml-latest-small"

## Only extract the data the first time the script is run.
if not movielens_dir.exists():
    with ZipFile(movielens_zipped_file, "r") as zip:
        zip.extractall(path=keras_datasets_path) # extract all members in a ZIP file

 



사용자 별 영화 평가점수('ratings.csv')와 영화 정보('movies.csv') 데이터셋을 사용해서 영화 추천 (movie recommentation) 에 사용할 수 있습니다.



print('datasets path:', keras_datasets_path)

[Out] datasets path: /Users/ihongdon/.keras/datasets


print(os.listdir(keras_datasets_path))

[Out] ['cowper.txt', 'reuters_word_index.json', 'imdb_word_index.json', 'flower_photos.tar.gz', 'cifar-10-batches-py', 'mnist.npz', 'ml-latest-small.zip', 'ml-latest-small', 'fashion-mnist', 'butler.txt', 'imdb.npz', 'cifar-10-batches-py.tar.gz', 'boston_housing.npz', 'creditcard.csv', 'creditcard.zip', 'derby.txt', 'train.csv', 'flower_photos', 'reuters.npz', 'fsns.tfrec']

os.listdir(movielens_dir)

[Out] ['links.csv', 'tags.csv', 'ratings.csv', 'README.txt', 'movies.csv']



[Reference]

* zipfile -Work with ZIP archives: https://docs.python.org/3/library/zipfile.html


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

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



728x90
반응형
Posted by Rfriend
,

pandas의 Series나 DataFrame 자료구조로 저장된 시계열 데이터에 대해서 이전 값 대비 현재 값의 변동율(change percentage)을 구하고 싶을 때 pandas 의 pct_change() 메소드를 사용하면 매우 편리하게 계산할 수 있습니다. 


이번 포스팅에서는 Python pandas 패키지의 pct_change() 메소드를 사용하여 


pandas Series에서

- (1) 이전 원소 대비 변동률 

       (Percentage change between the current and a prior element)

- (2) 이전 2개 원소 대비 변동률 

       (Percentage change between the current and 2 periods prior element)

- (3) 결측값을 이전 원소 값으로 대체 후 이전 원소 대비 변동률 

       (Percentage change between the current and a prior element after filling the missing values using the 'forward fill' method)


pandas DataFrame에서

- (4) 그룹별 이전 분기 대비 변동률 

      (Percentage change between the current and a prior quarter by Group)

- (5) 그룹별 전년 동분기 대비 변동률  

       (Percentage change between the current and a year before by Group)




* pandas의 pct_change() 메소드는 Series와 DataFrame 자료구조 모두에서 동일하게 사용 가능합니다. 



-- pandas Series 에서


  (1) 이전 원소 대비 변동률 

       (Percentage change between the current and a prior element)


pandas의 pct_change() 메소드는 기본 설정이 이전 원소 대비 현재 원소의 변동 비율(percentage change)을 계산해줍니다. 아래 pandas Series의 경우, 


첫번째 값은 이전 값이 없으므로 NaN

두번째 값의 첫번째 값 대비 변동률 = (20-10)/10 = 1.0

세번째 값의 두번째 값 대비 변동률 = (50-20)/20 = 1.5

네번째 값의 세번째 값 대비 변동률 = (55-50)/50 = 0.1

다섯번째 값의 네번째 값 대비 변동률 = (70-55)/55 = 0.27



In [1]: import pandas as pd


In [2]:

s = pd.Series([10, 20, 50, 55, 70])

s.pct_change()


Out[2]:

0 NaN

1 1.000000

2 1.500000

3 0.100000

4 0.272727

dtype: float64





  (2) 이전 2개 원소 대비 변동률 

       (Percentage change between the current and 2 periods prior element)


변동률을 구할 때 이전 값의 이동 기간을 periods 매개변수를 사용하면 자유롭게 설정해줄 수 있습니다. 가령, 위의 s Series 예에서 이전 2개 원소 대비 변동률은 s.pct_change(periods=2) 로 해주면 됩니다. 


첫번째와 두번째 값은 이전 2개 원소 값이 없으므로 NaN

세번째값의 이전 2개 원소 값 대비 변동률 = (50-10)/10 = 4.0

네번째값의 이전 2개 원소 값 대비 변동률 = (55-20)/20 = 1.75

다섯번째값의 이전 2개 원소 값 대비 변동률 = (70-50)/50 = 0.4



In [3]:

s = pd.Series([10, 20, 50, 55, 70])

s.pct_change(periods=2)


Out[3]:

0 NaN

1 NaN

2 4.00

3 1.75

4 0.40

dtype: float64





  (3) 결측값을 이전 원소 값으로 대체 후 이전 원소 대비 변동률 

       (Percentage change between the current and a prior element
        after filling the missing values using the 'forward fill' method
)


만약 데이터셋 안에 결측값(missing value)가 있다면 pct_change() 메소드에 pandas의 결측값 처리 매개변수를 그대로 차용하여 결측값을 처리한 후에 이전 원소 대비 변동률을 구할 수 있습니다. 


결측값을 처리하는 방법으로는, 

fill_method='ffill' or 'pad'       : 이전 값으로 결측값을 대체하여 채우기 (앞방향으로 채워나가기)

fill_method='bfill' or 'backfill'  : 이후 값으로 결측값을 대체하여 채우기 (뒤방향에서 채워나가기)



In [4]:

s2 = pd.Series([10, 20, 50, None, 70])

s2.pct_change(fill_method='ffill')


Out[4]:

0 NaN

1 1.0

2 1.5

3 0.0

4 0.4

dtype: float64

 




-- pandas DataFrame 에서


  (4) 그룹별 이전 분기 대비 변동률 

      (Percentage change between the current and a prior quarter by Group)


예제로 사용할 '제품(product)' 그룹을 가진 연도(year)/ 분기(quarter)  기간 별 판매량(sales) 칼럼으로 구성된 DataFrame을 만들어보겠습니다. 



In [5]:

# input data sale = pd.DataFrame( {'product': ['a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b'], 'year': [2018, 2018, 2018, 2018, 2019, 2019, 2019, 2019, 2020, 2020, 2020, 2020, 2018, 2018, 2018, 2018, 2019, 2019, 2019, 2019, 2020, 2020, 2020, 2020], 'quarter': [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], 'sales': [5, 6, 6, 8, 10, 20, 30, 40, 12, 25, 38, 50, 60, 65, 80, 95, 100, 125, 130, 140, 110, 130, 132, 144]})


sale

Out[5]:

product year quarter sales

0 a 2018 1 5

1 a 2018 2 6

2 a 2018 3 6

3 a 2018 4 8

4 a 2019 1 10

5 a 2019 2 20

6 a 2019 3 30

7 a 2019 4 40

8 a 2020 1 12

9 a 2020 2 25

10 a 2020 3 38

11 a 2020 4 50

12 b 2018 1 60

13 b 2018 2 65

14 b 2018 3 80

15 b 2018 4 95

16 b 2019 1 100

17 b 2019 2 125

18 b 2019 3 130

19 b 2019 4 140

20 b 2020 1 110

21 b 2020 2 130

22 b 2020 3 132

23 b 2020 4 144

 



이제 '제품(product)' 그룹별로 '이전 분기 대비 현재 분기의 변동율(change percentage between the current and the prior quarter)' 을 구해보겠습니다. 


물론 이번 예제 데이터는 년(year)/ 분기(quarter) 를 기준으로 이미 정렬이 되어 있기는 합니다만, 정렬이 안되어 있는 경우도 있을 수 있으므로 명확하게 시간 기준으로 정렬될 수 있도록 sort_values(by=['year', 'quarter'] 로 명시적으로 먼저 정렬을 해주었습니다.  다음으로 groupby(['product']) 메소드로 '제품(product)' 별로 그룹을 분할(split) 하여 그룹별로 이후 연산이 이루어지도록 하였습니다. 마지막으로 sales.pct_change() 메소드로 '판매량(sales)' 칼럼에 대해 '이전대비 변동률(pct_change)'을 '제품' 그룹별로 구해주었습니다. 



In [6]:

sale['sales_pct_change_by_1q'] = sale.\ sort_values(['year', 'quarter']).\ groupby(['product']).\ sales.pct_change()


sale

Out[6]:

product year quarter sales pct_change_by_1q

0 a 2018 1 NaN

1 a 2018 2 6 0.200000

2 a 2018 3 6 0.000000

3 a 2018 4 8 0.333333

4 a 2019 1 10 0.250000

5 a 2019 2 20 1.000000

6 a 2019 3 30 0.500000

7 a 2019 4 40 0.333333

8 a 2020 1 12 -0.700000

9 a 2020 2 25 1.083333

10 a 2020 3 38 0.520000

11 a 2020 4 50 0.315789

12 b 2018 1 60 NaN

13 b 2018 2 65 0.083333

14 b 2018 3 80 0.230769

15 b 2018 4 95 0.187500

16 b 2019 1 100 0.052632

17 b 2019 2 125 0.250000

18 b 2019 3 130 0.040000

19 b 2019 4 140 0.076923

20 b 2020 1 110 -0.214286

21 b 2020 2 130 0.181818

22 b 2020 3 132 0.015385

23 b 2020 4 144 0.090909

 




  (5) 그룹별 전년 동분기 대비 변동률  

       (Percentage change between the current and a year before by Group)


만약 이전 분기가 아니라 '전년 동일 분기' 대비 변동률을 구하고 싶다면 pct_change(periods=4) 처럼 periods=4 매개변수를 설정해주어서 4분기 이전 (즉, 전년 동일 분기)의 값 대비 변동률을 구해주면 됩니다. (만약 월 단위로 데이터가 집계되어 있다면 pct_change(periods=12) 로 해주면 됩니다.)



In [7]:

 sale['pct_change_by_1y'] = sale.sort_values(['year', 'quarter']).\

  groupby(['product']).\

  sales.pct_change(periods=4)


In [8]: sale.sort_values(by=['product', 'quarter', 'year'])

Out[8]:

product year quarter sales pct_change_by_1q pct_change_by_1y

0 a 2018 1 5 NaN NaN

4 a 2019 1 10 0.250000 1.000000

8 a 2020 1 12 -0.700000 0.200000

1 a 2018 2 6 0.200000 NaN

5 a 2019 2 20 1.000000 2.333333

9 a 2020 2 25 1.083333 0.250000

2 a 2018 3 6 0.000000 NaN

6 a 2019 3 30 0.500000 4.000000

10 a 2020 3 38 0.520000 0.266667

3 a 2018 4 8 0.333333 NaN

7 a 2019 4 40 0.333333 4.000000

11 a 2020 4 50 0.315789 0.250000

12 b 2018 1 60 NaN NaN

16 b 2019 1 100 0.052632 0.666667

20 b 2020 1 110 -0.214286 0.100000

13 b 2018 2 65 0.083333 NaN

17 b 2019 2 125 0.250000 0.923077

21 b 2020 2 130 0.181818 0.040000

14 b 2018 3 80 0.230769 NaN

18 b 2019 3 130 0.040000 0.625000

22 b 2020 3 132 0.015385 0.015385

15 b 2018 4 95 0.187500 NaN

19 b 2019 4 140 0.076923 0.473684

23 b 2020 4 144 0.090909 0.028571

 



또는 아래 방법처럼 분기(quarter)/ 년(year) 를 기준으로 먼저 정렬을 해놓고, 그 다음에 제품/분기 그룹(groupby(['product', 'quarter']) 별로 판매량의 변동률(sales.pct_change())를 구해도 결과는 같습니다. 



# or equvalently

sale['pct_change_by_1y'] = sale.sort_values(by=['quarter', 'year']).\

    groupby(['product', 'quarter']).\

        sales.pct_change()


sale.sort_values(by=['product', 'quarter', 'year'])

 



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

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



728x90
반응형
Posted by Rfriend
,

희소행렬(Sparse matrix)은 대부분의 값이 원소 '0'인 행렬, '0'이 아닌 원소가 희소(sparse)하게, 듬성듬성 있는 행렬을 말합니다. 반대로 밀집행렬(Dense matrix)은 대부분의 원소 값이 '0'이 아닌 행렬을 말합니다. 


자연어처리 분석을 할 때 문서 내 텍스트를 컴퓨터가 이해할 수 있는 형태의 자료구조로 만들 때 텍스트 파싱을 거쳐 단어-문서 행렬(Term-Document matrix) (or 문서-단어 행렬, Document-Term matrix) 를 만드는 것부터 시작합니다. 


문서별로 많은 수의 단어가 포함되어 있고, 또 단어별로 발생 빈도가 보통은 드물기 때문에, 문서에서 단어를 파싱하여 Term-Document 행렬을 만들면 대부분은 희소행렬(Sparse matrix)을 얻게 됩니다. 


이번 포스팅에서는 


(1) 문서별 단어로 부터 CSR 행렬(Compressed Sparse Row matrix) 을 만들고, 

(2) CSR 행렬을 이용해 NumPy array의 Term-Document 행렬 만들기


를 해보겠습니다. 




단, 이번 포스팅의 주 목적은 문서로부터 문서-단어 CSR 행렬을 만들고 --> 이를 NumPy array의 Term-Document 행렬을 만드는 과정에 집중해서 소개하는 것으로서, 텍스트 파싱하는데 필요한 세부 절차(가령 문장 분리, 대문자의 소문자로 변환, Stop words 생략 등)는 생략합니다. 

(텍스트를 단어 단위로 파싱해서 one-hot encoding 하는 방법은 https://rfriend.tistory.com/444 포스팅 참조하세요.)



  (1) 문서별 단어로 부터 CSR 행렬(Compressed Sparse Row matrix) 을 만들기


먼저, NumPy와 SciPy 모듈을 importing 하겠습니다. 



import numpy as np

from scipy.sparse import csr_matrix

 



아래와 같이 리스트 [] 하나를 문서(Document) 하나로 간주했을 때, 총 3개의 문서를 가진 "docs" 로 부터 단어(Term)를 파싱해서 단어집(Vocabulary) 사전형(dictionary)을 만들고, 압축 희소 행기준 행렬(Compressed Sparse Row matrix) 을 만들기 위해 필요한 indptr, indices, data 객체를 for loop 문을 써서 만들어보겠습니다. 


참고로, CSR 행렬 소개, SciPy.sparse.csr_matrix() 메소드 소개, NumPy 희소행렬을 SciPy 압축 희소 행기준 행렬 (Compressed Sparse Row matrix) 로 변환하는 방법은 https://rfriend.tistory.com/551 포스팅을 참고하세요. 



# To construct a CSR matrix incrementally

docs = [["python", "is", "a", "programming", "language"], 

        ["programming", "is", "fun"], 

        ["python", "is", "easy"]]


indptr = [0]

indices = []

data = []

vocabulary = {}


for d in docs:

    for term in d:

        index = vocabulary.setdefault(term, len(vocabulary))

        indices.append(index)

        data.append(1)

    indptr.append(len(indices))



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



위의 실행결과로 얻은 단어집(Vocabulary)을 Key : Value 쌍으로 출력을 해서 살펴보겠습니다. 3개의 문서에 총 7개의 단어가 있군요. (문서별로 중복되는 단어(term)가 존재함)


 

for k, v in vocabulary.items():

    print(k, ':', v)


[Out]
python : 0
is : 1
a : 2
programming : 3
language : 4
fun : 5
easy : 6




위에서 얻은 indptr, indices, data 를 가지고 SciPy.sparse.csr_matrix() 메소드를 이용하여 압축 희소 행기준 행렬(CSR matrix)을 만들어보겠습니다.  



term_document_csr_mat = csr_matrix((data, indices, indptr), dtype=int)

term_document_csr_mat

[Out] <3x7 sparse matrix of type '<class 'numpy.int64'>'
	with 11 stored elements in Compressed Sparse Row format>


print(term_document_csr_mat)

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



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

print('indptr:', term_document_csr_mat.indptr)

print('indices:', term_document_csr_mat.indices)

print('data:', term_document_csr_mat.data)


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





  (2) CSR 행렬을 이용해 NumPy array의 Term-Document 행렬 만들기


위의 (1)번에서 만든 SciPy CSR(Compressed Sparse Row) matrix를 csr_matrix.toarray() 또는 csr_matrix.todense() 메소드를 사용해서 NumPy array 행렬로 변환해보겠습니다. 이로부터 Term-Document Matrix를 만들었습니다. 



# converting SciPy CSR matrix to NumPy array

term_document_arr = term_document_mat.toarray() # or todense()


term_document_arr

[Out]
array([[1, 1, 1, 1, 1, 0, 0],
       [0, 1, 0, 1, 0, 1, 0],
       [1, 1, 0, 0, 0, 0, 1]])




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

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



728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 (1) 파이썬의 자료형 중에서도 사전형(Dictionary)을 키와 값 (Key and Value) 의 쌍으로 인쇄를 할 때 좀더 가독성을 좋게 하도록 키(Key) 에서 일정 간격을 띄우고 나서 값(Value)을 인쇄하는 옵션을 소개하겠습니다.  


그리고 소소한 팁으로 (2) 파이썬 인쇄 escape 옵션, (3) 파이썬 인쇄 sep, end 옵션을 추가로 소개하겠습니다. 



  (1) 파이썬 사전형의 키와 값을 일정 간격을 두고 인쇄하기

      (Print Key, Value of Python Dictionary with fixed space)




먼저, 예제로 사용할 간단한 파이썬 사전형(Dictionary) 객체를 키와 값의 쌍(pair of key and value)으로 만들어보고 print() 로 인쇄를 해보겠습니다


사전형 객체에 대해서 그냥 print() 로 인쇄를 하면 옆으로 길게 쭈욱~ 늘어서 인쇄가 되기 때문에 아무래도 한눈에 보기가 쉽지 않습니다. 



my_dict = {'first name': 'KilDong.', 

           'last name': 'Hong.',

           'age': '30.', 

           'address': 'Seoul, Korea.'}



print(my_dict)

[Out]

{'first name': 'KilDong.', 'last name': 'Hong.', 'age': '30.', 'address': 'Seoul, Korea.'}





그러면, 좀더 보기에 좋도록 이번에는 dictionary.items() 로 키와 값을 분해해서 각각 가져오고, for loop 순환문으로 반복을 하면서 각 키와 값을 한줄에 하나씩 인쇄를 해보겠습니다. 이때 format() 메소드를 사용해서 Key, Value 값을 인쇄할 때 각각 치환해주었습니다. 

바로 앞의 예에서 그냥 print() 한 것보다는 한결 보기에 좋습니다만, Key 문자열의 길이가 들쭉날쭉 하다보니 Key : Value 로 쌍을 이루어서 인쇄를 할 때 Value 인쇄가 시작하는 위치도 역시 들쭉날쭉해서 좀 정신이 없고 눈에 잘 안들어오는 한계가 있습니다. 



for k, v in my_dict.items():

    print("{} : {}".format(k, v))


[Out]
first name : KilDong.
last name : Hong.
age : 30.
address : Seoul, Korea.





이럴 때 {!r:15s} 으로 특정 숫자만큼의 string 간격을 두고 인쇄하라는 옵션을 추가해주면 아래와 같이 Key 문자열의 시작위치부터 15 string 만큼 각격을 두고, 이어서 다음의 문자열(여기서는 ': {value}')을 인쇄하게 됩니다. 위의 예보다는 아래의 예가 한결 Key : Value 쌍을 한눈에 보기에 좋아졌습니다.   



for k, v in my_dict.items():

    print("{!r:15s} : {}".format(k, v))


[Out]

'first name' : KilDong. 'last name' : Hong. 'age' : 30. 'address' : Seoul, Korea.

 





  (2) 파이썬 인쇄 escape 옵션 (Python Print escape options)

파이썬 문법을 탈출(escape)하여 인쇄할 수 있는 소소한 옵션들이 몇 개 있습니다. 

(아래에서 \ 는 역슬래쉬  , back-slash 입니다)


  • \n : 새로운 줄로 바꾸어서 인쇄하기
  • \t : 탭(tab)으로 들여쓰기해서 인쇄하기 (오른쪽으로 탭한 만큼 밀어서 인쇄)
  • \b : 뒤에서 한칸 back-space 하여 인쇄하기 (제일 뒤에 문자가 있으면 삭제되어서 인쇄)
  • \" : 큰 따옴표(") 인쇄하기
  • \' : 작은 따옴표(') 인쇄하기
  • \\ : 역슬래쉬('\') 인쇄하기


  • \n : 새로운 줄로 바꾸어서 인쇄하기


for k, v in my_dict.items():

    print("\n{} : {}".format(k, v))


[Out]

first name : KilDong.

last name : Hong.

age : 30.

address : Seoul, Korea.





  • \t : 탭(tab)으로 들여쓰기해서 인쇄하기 (오른쪽으로 탭한 만큼 밀어서 인쇄)


for k, v in my_dict.items():

    print("\t{} : {}".format(k, v))


[Out]
	first name : KilDong.
	last name : Hong.
	age : 30.
	address : Seoul, Korea.





  • \b : 뒤에서 한칸 back-space 하여 인쇄하기 (제일 뒤에 문자가 있으면 삭제되어서 인쇄)



for k, v in my_dict.items():

    print("{} : {}\b".format(k, v))


[Out]
first name : KilDong
last name : Hong
age : 30
address : Seoul, Korea

 




  • \" : 큰 따옴표(") 인쇄하기


for k, v in my_dict.items():

    print("{} : \"{}\"".format(k, v))


[Out]
first name : "KilDong."
last name : "Hong."
age : "30."
address : "Seoul, Korea."

 




  • \\ : 역슬래쉬('\') 인쇄하기


for k, v in my_dict.items():

    print("{} : \\{}\\".format(k, v))


[Out]
first name : \KilDong.\
last name : \Hong.\
age : \30.\
address : \Seoul, Korea.\

 





  (3) 파이썬 인쇄 sep, end 옵션 (Python Print sep, end options)


  • sep="separator" 옵션 : 두 개의 문자열 사이에 구분자(separator) 문자로 구분하여 붙여서 인쇄
  • end="end_string" 옵션 : 앞의 문자열에 바로 이어서 end_string을 붙여서 인쇄


앞의 (1)번에서 예로 들었던 Key : Value 쌍으로 item 한줄씩 인쇄하는 것을 sep, end 옵션을 사용해서 똑같이 재현해보겠습니다. 


  • sep="separator" 옵션 : 두 개의 문자열 사이에 구분자(separator) 문자로 구분하여 붙여서 인쇄


for k, v in my_dict.items():

    print(k, v, sep=" : ")


[Out]

first name : KilDong. last name : Hong. age : 30. address : Seoul, Korea.

 




  • end="end_string" 옵션 : 앞의 문자열에 바로 이어서 end_string을 붙여서 인쇄


for k, v in my_dict.items():

    print(k + " : ", end=v+"\n")


[Out]
first name : KilDong.
last name : Hong.
age : 30. 

address : Seoul, Korea.




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

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



728x90
반응형
Posted by Rfriend
,

행렬의 값이 대부분 '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



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

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



728x90
반응형
Posted by 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])

 



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

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



728x90
반응형
Posted by 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/)


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

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



728x90
반응형
Posted by Rfriend
,