이전 포스팅의  rfriend.tistory.com/262 에서는 Python pandas DataFrame의 결측값을 fillna() 메소드를 사용해서 특정 값으로 채우거나 평균으로 대체하는 방법을 소개하였습니다. 

 

이번 포스팅에서는 Python pandas DataFrame 의 결측값을 선형회귀모형(linear regression model) 을  사용하여 예측/추정하여 채워넣는 방법을 소개하겠습니다. (물론, 아래의 동일한 방법을 사용하여 선형회귀모형 말고 다른 통계, 기계학습 모형을 사용하여 예측/추정한 값으로 대체할 수 있습니다.)

 

(1) 결측값을 제외한 데이터로부터 선형회귀모형 훈련하기

    (training, fitting a linear regression model using non-missing values)

(2) 선형회귀모형으로 부터 추정값 계산하기 (prediction using linear regression model)

(3) pandas 의 fillna() 메소드 또는  numpy의  np.where()  메소드를 사용해서 결측값인 경우 선형회귀모형 추정값으로 대체하기 (filling missing values using the predicted values by linear regression model)

 

fill missing values of pandas DataFrame using predicted values by machine learning model

 

아래에는 예제로 사용할 데이터로 전복(abalone) 공개 데이터셋을 읽어와서 1행~3행의 'whole_weight' 칼럼 값을 결측값(NA) 으로 변환해주었습니다. 

import pandas as pd
import numpy as np

# read abalone dataset from website
abalone = pd.read_csv("http://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data", 
                 header=None, 
                 names=['sex', 'length', 'diameter', 'height', 
                          'whole_weight', 'shucked_weight', 
                          'viscera_weight', 'shell_weight', 'rings'])
                          

# get 10 observations as an example
df = abalone.copy()[:10]


# check missing values : no missing value at all
pd.isnull(df).sum()
# sex               0
# length            0
# diameter          0
# height            0
# whole_weight      0
# shucked_weight    0
# viscera_weight    0
# shell_weight      0
# rings             0
# dtype: int64


# insert NA values as an example
df.loc[0:2, 'whole_weight'] = np.nan

df
# sex	length	diameter	height	whole_weight	shucked_weight	viscera_weight	shell_weight	rings
# 0	M	0.455	0.365	0.095	NaN	0.2245	0.1010	0.150	15
# 1	M	0.350	0.265	0.090	NaN	0.0995	0.0485	0.070	7
# 2	F	0.530	0.420	0.135	NaN	0.2565	0.1415	0.210	9
# 3	M	0.440	0.365	0.125	0.5160	0.2155	0.1140	0.155	10
# 4	I	0.330	0.255	0.080	0.2050	0.0895	0.0395	0.055	7
# 5	I	0.425	0.300	0.095	0.3515	0.1410	0.0775	0.120	8
# 6	F	0.530	0.415	0.150	0.7775	0.2370	0.1415	0.330	20
# 7	F	0.545	0.425	0.125	0.7680	0.2940	0.1495	0.260	16
# 8	M	0.475	0.370	0.125	0.5095	0.2165	0.1125	0.165	9
# 9	F	0.550	0.440	0.150	0.8945	0.3145	0.1510	0.320	19

 

 

 

(1) 결측값을 제외한 데이터로부터 선형회귀모형 훈련하기  (training, fitting a linear regression model using non-missing values)

 

 pandas 패키지의 dropna() 메소드를 이용해서 결측값이 포함된 행을 제거한 후의 설명변수  ' diameter', 'height', 'shell_weight' 를 'X' DataFrame 객체로 만들고, ' whole_weight' 를 종속변수  'y' Series로 만든 후에,  sklearn의  linear_model.LinearRegression() 메소드로   lin_reg.fit(X, y) 로 선형회귀모형을 적합하였습니다. 

 

# initiate sklearn's linear regression
from sklearn import linear_model

lin_reg = linear_model.LinearRegression()


# X and y after excluding missing values
X = df.dropna(axis=0)[['diameter', 'height', 'shell_weight']] 
y = df.dropna(axis=0)['whole_weight'] 


# fitting linear regression model using non-missing values
lin_reg_model = lin_reg.fit(X, y)

 

 

 

(2) 선형회귀모형으로 부터 추정값 계산하기 (prediction using linear regression model)

 

위의 (1)번에서 적합한 모델에 predict() 함수를 사용해서  'whole_weight'  의 값을 추정하였습니다. 

 

# Prediction
y_pred = lin_reg_model.predict(df.loc[:, ['diameter', 'height', 'shell_weight']])

y_pred
# array([0.54856977, 0.21868994, 0.69091523, 0.50734984, 0.19206521,
#        0.35618402, 0.80347213, 0.7804138 , 0.53164895, 0.85086606])

 

 

 

(3) pandas 의 fillna() 메소드 또는  numpy의  np.where()  메소드를 사용해서 결측값인 경우 선형회귀모형 추정값으로 대체하기 (filling missing values using the predicted values by linear regression model)

 

(방법 1)  pandas  의  fillna()  메소드를 사용해서  'whole_weight' 값이 결측값인 경우에는  위의 (2)번에서 선형회귀모형을 이용해 추정한 값으로 대체를 합니다. 이때  'y_pred' 는  2D numpy array 형태이므로, 이를 flatten() 메소드를 사용해서  1D array 로 바꾸어주고, 이를  pd.Series() 메소드를 사용해서 Series 데이터 유형으로 변환을 해주었습니다.   inplace=True 옵션을 사용해서 df DataFrame 내에서 결측값이 선형회귀모형 추정값으로 대체되고 나서 저장되도록 하였습니다. 

 

(방법 2)  numpy의 where() 메소드를 사용해서,  결측값인 경우  (즉,  isnull() 이 True)  pd.Series(y_pred.flatten()) 값을 가져옥, 결측값이 아닌 경우 기존 값을 가져와서  'whole_weight' 에 값을 할당하도록 하였습니다. 

 

(방법 3) for loop 을 돌면서 매 행의  'whole_weight' 값이 결측값인지 여부를 확인 후,  만약  결측값이면 (isnull() 이 True 이면) 위의 (1)에서 적합된 회귀모형에 X값들을 넣어줘서 예측을 해서 결측값을 채워넣는 사용자 정의함수를 만들고 이를  apply() 함수로 적용하는 방법도 생각해볼 수는 있으나, 데이터 크기가 큰 경우  for loop 연산은 위의 (방법 1), (방법 2) 의   vectorized operation 대비 성능이 많이 뒤떨어지므로 소개는 생략합니다. 

 

## filling missing values using predicted values by a linear regression model

## -- (방법 1) pd.fillna() methods
df['whole_weight'].fillna(pd.Series(y_pred.flatten()), inplace=True)


## -- (방법 2) np.where()
df['whole_weight'] = np.where(df['whole_weight'].isnull(), 
                              pd.Series(y_pred.flatten()), 
                              df['whole_weight'])
                              
## results
df
# sex	length	diameter	height	whole_weight	shucked_weight	viscera_weight	shell_weight	rings
# 0	M	0.455	0.365	0.095	0.548570	0.2245	0.1010	0.150	15
# 1	M	0.350	0.265	0.090	0.218690	0.0995	0.0485	0.070	7
# 2	F	0.530	0.420	0.135	0.690915	0.2565	0.1415	0.210	9
# 3	M	0.440	0.365	0.125	0.516000	0.2155	0.1140	0.155	10
# 4	I	0.330	0.255	0.080	0.205000	0.0895	0.0395	0.055	7
# 5	I	0.425	0.300	0.095	0.351500	0.1410	0.0775	0.120	8
# 6	F	0.530	0.415	0.150	0.777500	0.2370	0.1415	0.330	20
# 7	F	0.545	0.425	0.125	0.768000	0.2940	0.1495	0.260	16
# 8	M	0.475	0.370	0.125	0.509500	0.2165	0.1125	0.165	9
# 9	F	0.550	0.440	0.150	0.894500	0.3145	0.1510	0.320	19

 

 

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

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

 

728x90
반응형
Posted by Rfriend
,

이전 포스팅에서는 numpy 배열의 원소 값을 사전(dictionary)의 (Key, Value)를 매핑해서 변환하는 방법을 소개하였습니다. (rfriend.tistory.com/620)

 

이번 포스팅에서는 Python numpy 의 array 배열의 순서대로 정수를 사전의 키(Key)로 하고, 배열 값을 사전의 값(Value)으로 하는 Python 사전(dictionary) 으로 변환하는 몇 가지 방법을 소개하겠습니다.

 

(1) dict() 와 enumerate() 함수를 이용해 배열로 부터 사전 만들기

(2) for loop 과 enumerate() 함수를 이용해 배열로 부터 사전 만들기

 

 

 

(1) dict() 와 enumerate() 함수를 이용해 배열로 부터 사전 만들기

 

먼저, numpy 라이브러리를 불러오고, 예제로 사용할 (5, 0) shape 의 numpy array 배열을 하나 만들어보겠습니다.

 

import numpy as np

cls_weight = np.array([0.30, 0.50, 0.10, 0.03, 0.07])
cls_weight
[Out]
array([0.3 , 0.5 , 0.1 , 0.03, 0.07])

cls_weight.shape
[Out] 
(5,)

 

 

위의 'cls_weight' 배열을 사전(dictionary)으로 변환해보겠습니다. 사전(dict) 키(Key)가 '0' 부터 시작하고, 배열의 순서대로 사전의 키가 하나씩 증가하며, 배열의 순서대로 사전에 값을 할당하여 보겠습니다.  dict() 함수는 객체를 '키(Key) : 값(Value)' 의 쌍을 가지는 사전형 자료구조를 만들어줍니다.

 

## converting numpy array to dictionary, 
## dict key is starting from 0
cls_weight_dict_from_0 = dict(enumerate(cls_weight))

cls_weight_dict_from_0
[Out]
{0: 0.3, 1: 0.5, 2: 0.1, 3: 0.03, 4: 0.07}

 

 

이때 dict() 안의 enumerate() 메소드는 객체를 순환할 때 회수를 세어주는 counter 를 같이 생성해서 enumerate 객체를 반환합니다. for loop 으로 enumerate 객체를 순환하면서 counter 와 배열 내 값을 차례대로 출력을 해보면 아래와 같습니다.

## enumerate() method adds a counter to an iterable 
## and returns it in a form of enumerate object
for i, j in enumerate(cls_weight):
    print(i, ':', j)
    
[Out]
0 : 0.3
1 : 0.5
2 : 0.1
3 : 0.03
4 : 0.07

 

 

경우에 따라서는 배열의 값으로 사전을 만들었을 때, 사전의 키 값이 '0'이 아니라 '1'이나 혹은 다른 숫자로 부터 시작하는 것을 원할 수도 있습니다. 이럴 경우 enumerate(iterable_object, 1) 처럼 원하는 숫자(아래 예에서는 '1')를 추가해주면 그 값이 더해져서 counter 가 생성이 됩니다.

 

## converting numpy array to dictionary, 
## dict key is starting from 1

cls_weight_dict_from_1 = dict(enumerate(cls_weight, 1))

cls_weight_dict_from_1
[Out]
{1: 0.3, 2: 0.5, 3: 0.1, 4: 0.03, 5: 0.07}

 

 

만약 사전(dictionary)으로 변환하려고 하는 numpy array의 axis 1의 축이 있다면 flatten() 메소드를 사용해서 axis 0 만 있는 배열로 먼저 평평하게 펴준 (axis 1 축을 없앰) 후에 위의 dict(enumerate()) 를 똑같이 사용해주면 됩니다.  아래 예는 shape (5, 1) 의 배열을 flatten() 메소드를 써서 shape (5, 0) 으로 바꿔준 후에 dict(enumerate()) 로 배열을 사전으로 변환해주었습니다.

 

## array with axis1
cls_weight_2 = np.array([[0.30], [0.50], [0.10], [0.03], [0.07]])
cls_weight_2
[Out]
array([[0.3 ],
       [0.5 ],
       [0.1 ],
       [0.03],
       [0.07]])


cls_weight_2.shape
[Out]
(5, 1)


## use flatten() method to convert shape (5, 1) to (5, 0)
cls_weight_dict_2 = dict(enumerate(cls_weight_2.flatten()))
print(cls_weight_dict_2)
[Out]
{0: 0.3, 1: 0.5, 2: 0.1, 3: 0.03, 4: 0.07}

 

 

 

(2) for loop 과 enumerate() 함수를 이용해 배열로 부터 사전 만들기

 

이번에는 for loop 과 enumerate() 메소드를 같이 이용하는 방법입니다. 위의 (1) 번 대비 좀 복잡한 느낌이 있기는 하지만, (1) 번 대비 (2) 방법은 for loop 안의 코드 블럭에 좀더 자유롭게 원하는 복잡한 로직을 녹여서 사전(dictionary)을 구성할 수 있다는 장점이 있습니다.

 

아래 예에서는 (a) 먼저 cls_weight_dict_3 = {} 로 비어있는 사전을 만들어 놓고, (b) for loop 으로 순환 반복을 하면서 enumerate(cls_weight) 가 반환해주는 (counter, 배열값) 로 부터 counter 정수 숫자를 받아서 cls_weight_dict_3 의 키(Key) 로 할당해주고, 배열의 값을 사전의 해당 키에 할당해주는 방식입니다.  사전의 키에 값 할당(assinging Value to dict by mapping Key)은 Dict[Key] = Value 구문으로 해줍니다.

 

cls_weight = np.array([0.30, 0.50, 0.10, 0.03, 0.07])
cls_weight
[Out]
array([0.3 , 0.5 , 0.1 , 0.03, 0.07])

## Converting a numpy array to a dictionary
## Dict key is starting from 0
cls_weight_dict_3 = {}

for i, c_w in enumerate(cls_weight):
    cls_weight_dict_3[i] = c_w
    

print(cls_weight_dict_3)
[Out]
{0: 0.3, 1: 0.5, 2: 0.1, 3: 0.03, 4: 0.07}

 

 

사전의 키를 '0' 이 아니라 '1'부터 시작하게 하려면 enumerate()의 counter가 0부터 시작하므로, counter를 사전의 키에 할당할 때 'counter+1' 을 해주면 됩니다.

 

## converting a numpy array to a dictionary using for loop
## dict key is strating from 1

## null dict
cls_weight_dict_3_from_1 = {}

## assigning values by keys + 1
for i, c_w in enumerate(cls_weight):
    cls_weight_dict_3_from_1[i+1] = c_w
    
    
print(cls_weight_dict_3_from_1)
[Out]
{1: 0.3, 2: 0.5, 3: 0.1, 4: 0.03, 5: 0.07}

 

 

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

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

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 1차원 배열 내 고유한 원소 집합 (a set with unique elements) 을 찾고, 더 나아가서 고유한 원소별 개수(counts per unique elements)도 세어보고, 원소 개수를 기준으로 정렬(sorting)도 해보는 여러가지 방법을 소개하겠습니다.

 

 

(1) numpy 1D 배열 안에서 고유한 원소 집합 찾기
    (finding a set with unique elements in 1D numpy array)

(2) numpy 1D 배열 안에서 고유한 원소 별로 개수 구하기
    (counts per unique elements in 1D numpy array)

(3) numpy 1D 배열 안에서 고유한 원소(key) 별 개수(value)를 사전형으로 만들기

    (making a dictionary with unique sets and counts of 1D numpy array)

(4) numpy 1D 배열의 고유한 원소(key) 별 개수(value)의 사전을 정렬하기

    (sorting a dictionary with unique sets and counts of 1D numpy array)

(5) numpy 1D 배열을 pandas Series 로 변환해서 고유한 원소 별 개수 구하고 정렬하기

    (converting 1D array to pandas Series, and value_counts(), sort_values())

(6) numpy 1D 배열을 pandas DataFrame으로 변환해 고유 원소별 개수 구하고 정렬하기

    (converting 1D array to pandas DataFrame, and value_counts(), sort_values())

 

 

 

 

먼저, 예제로 사용할 간단한 numpy 1D 배열을 만들어보겠습니다.

 

## simple 1D numpy array

import numpy as np

arr = np.array(['a', 'c', 'c', 'b', 'a', 
                'b', 'b', 'c', 'a', 'c', 
                'b', 'a', 'a', 'a', 'c'])
                
                
arr
[Out] array(['a', 'c', 'c', 'b', 'a', 'b', 'b', 'c', 'a', 'c', 
             'b', 'a', 'a', 'a', 'c'], dtype='<U1')
             

 

 

(1) numpy 1D 배열 안에서 고유한 원소 집합 찾기
    (finding a set with unique elements in 1D numpy array)

 

np.unique() 메소드를 사용하면 numpy 배열 내 고유한 원소(unique elements)의 집합을 찾을 수 있습니다.

 

## np.unique(): Find the unique elements of an array
np.unique(arr)
[Out] 
array(['a', 'b', 'c'], dtype='<U1')

 

 

더 나아가서, return_inverse=True 매개변수를 설정해주면, 아래의 예처럼 numpy 배열 내 고유한 원소의 집합 배열과 함께 '고유한 원소 집합 배열의 indices 의 배열' 을 추가로 반환해줍니다.

따라서 이 기능을 이용하면 array(['a', 'c', 'c', 'b', 'a', 'b', 'b', 'c', 'a', 'c', 'b', 'a', 'a', 'a', 'c']) 를 ==> array([0, 2, 2, 1, 0, 1, 1, 2, 0, 2, 1, 0, 0, 0, 2]) 로 쉽게 변환할 수 있습니다.

 

## return_inverse=True: If True, also return the indices of the unique array
np.unique(arr, 
          return_inverse=True)
[Out]
(array(['a', 'b', 'c'], dtype='<U1'),
 array([0, 2, 2, 1, 0, 1, 1, 2, 0, 2, 1, 0, 0, 0, 2]))
 

 

 

 

(2) numpy 1D 배열 안에서 고유한 원소 별로 개수 구하기
    (counts per unique elements in 1D numpy array)

 

위의 (1)번에서 np.unique() 로 numpy 배열 내 고유한 원소의 집합을 찾았다면, return_counts = True 매개변수를 설정해주면 각 고유한 원소별로 개수를 구해서 배열로 반환할 수 있습니다.

 

## return_counts: If True, also return the number of times each unique item appears in ar.
np.unique(arr, 
          return_counts = True)     

[Out]
(array(['a', 'b', 'c'], dtype='<U1'), array([6, 4, 5]))

 

 

 

(3) numpy 1D 배열 안에서 고유한 원소(key) 별 개수(value)를 사전형으로 만들기

    (making a dictionary with unique sets and counts of 1D numpy array)

 

위의 (2)번에서 각 고유한 원소별 개수를 구해봤는데요, 이를 파이썬의 키:값 쌍 (key: value pair) 형태의 사전(dictionary) 객체로 만들어보겠습니다.

 

먼저 np.unique(arr, return_counts = True) 의 결과를 unique, counts 라는 이름의 array로 할당을 받고, 이를 zip(unique, counts) 으로 쌍(pair)을 만들어준 다음에, dict() 를 사용해서 사전형으로 변환해주었습니다.

 

## making a dictionary with unique elements and counts of 1D array
unique, counts = np.unique(arr, return_counts = True)
uniq_cnt_dict = dict(zip(unique, counts))

uniq_cnt_dict
[Out]
{'a': 6, 'b': 4, 'c': 5}

 

 

 

(4) numpy 1D 배열의 고유한 원소(key) 별 개수(value)의 사전을 정렬하기

    (sorting a dictionary with unique sets and counts of 1D numpy array)

 

위의 (3)번까지 잘 진행을 하셨다면 이제 (unique : counts) 쌍의 사전을 'counts' 의 값을 기준으로 오름차순 정렬(sorting a dict by value in ascending order) 또는 내림차순 정렬 (sorting a dict by value in descending order) 하고 싶은 마음이 생길 수 있는데요, 이럴 경우 sorted() 메소드를 사용하면 되겠습니다. (pytho dictionary 정렬 참조: rfriend.tistory.com/473)

 

## sorting a dictionary by value in ascending order
## -- reference: https://rfriend.tistory.com/473
sorted(uniq_cnt_dict.items(), 
       key = lambda x: x[1])
       
[Out]
[('b', 4), ('c', 5), ('a', 6)]


## sorting a dictionary by value in descending order
sorted(uniq_cnt_dict.items(), 
       reverse = True, 
       key = lambda x: x[1])
       
[Out]
[('a', 6), ('c', 5), ('b', 4)]

 

 

 

(5) numpy 1D 배열을 pandas Series 로 변환해 고유한 원소별 개수 구하고 정렬하기

    (converting 1D array to pandas Series, and value_counts(), sort_values())

 

pandas 의 Series 나 DataFrame으로 변환해서 데이터 분석 하는 것이 더 익숙하거나 편리한 상황에서는 pandas.Series(array) 나 pandas.DataFrame(array) 로 변환을 해서, value_count() 메소드로 원소의 개수를 세거나, sort_values() 메소드로 값을 기준으로 정렬을 할 수 있습니다.

 

import pandas as pd

## converting an array to pandas Series
arr_s = pd.Series(arr)
arr_s
[Out]
0     a
1     c
2     c
3     b
4     a
5     b
6     b
7     c
8     a
9     c
10    b
11    a
12    a
13    a
14    c
dtype: object


## counting values by unique elements of pandas Series
arr_s.value_counts()
[Out]
a    6
c    5
b    4
dtype: int64


## sorting by values in ascending order of pandas Series
arr_s.value_counts().sort_values(ascending=True)
[Out]
b    4
c    5
a    6
dtype: int64

 

 

(6) numpy 1D 배열을 pandas DataFrame으로 변환해 고유한 원소별 개수 구하고 정렬하기

    (converting 1D array to pandas DataFrame, and value_counts(), sort_values())

 

만약 pandas Series 내 고유한 원소별 개수를 구한 결과를 개수의 오름차순으로 정렬을 하고 싶다면 sort_values(ascending = True) 를 설정해주면 됩니다. (내림차순이 기본 설정, default to descending order)

 

import pandas as pd

## converting an array to pandas DataFrame
arr_df = pd.DataFrame(arr, columns=['x1'])
arr_df

[Out]
x1
0	a
1	c
2	c
3	b
4	a
5	b
6	b
7	c
8	a
9	c
10	b
11	a
12	a
13	a
14	c


## counting the number of unique elements in Series
arr_df['x1'].value_counts()
[Out]
a    6
c    5
b    4
Name: x1, dtype: int64


## # sorting by the counts of unique elements in ascending order
arr_df['x1'].value_counts().sort_values(ascending=True)
[Out]
b    4
c    5
a    6
Name: x1, dtype: int64

 

 

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

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

 

728x90
반응형
Posted by Rfriend
,

이번 포스팅에서는 Python numpy 의 배열의 원소 값을 사전(dictionary)의 {키: 값} 쌍 ({key: value} pair) 을 이용해서, 배열의 원소 값과 사전의 키를 매핑하여 사전의 값으로 배열의 원소값을 변환하는 방법을 소개하겠습니다.

 

아래의 예에서는 다중분류 (multi-class classification) 기계학습 모델로 부터 각 관측치가 5개 classes 별 속할 확률을 배열로 반환받은 상황을 가정하여 만들어보았습니다.

 

(1) 다중분류 확률 배열로 부터 최대값의 위치 인덱스 가져오기

(2) np.vectorize() 와 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

(3) for loop 과 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

 

 

 

(1) 다중분류 확률 배열로 부터 최대값의 위치 인덱스 가져오기

 

먼저, 5개 class를 가지는 다중분류 문제에서 5개 class 별 속할 확률을 기계학습 분류 모델로 부터 아래의 'pred_proba' 라는 이름의 배열로 얻었다고 가정해보겠습니다.

 

import numpy as np

## probability for each classes
pred_proba = np.array([[0., 0., 0.2, 0.8, 0.], 
                       [0.9, 0., 0., 0., 0.1], 
                       [0., 0., 0.6, 0.2, 0.2], 
                       [0., 0., 0.5, 0.3, 0.2], 
                       [0., 0.1, 0.3, 0., 0.6], 
                       [0., 0.4, 0., 0.3, 0.3]])

pred_proba
[Out]
array([[0. , 0. , 0.2, 0.8, 0. ],
       [0.9, 0. , 0. , 0. , 0.1],
       [0. , 0. , 0.6, 0.2, 0.2],
       [0. , 0. , 0.5, 0.3, 0.2],
       [0. , 0.1, 0.3, 0. , 0.6],
       [0. , 0.4, 0. , 0.3, 0.3]])

 

 

이들 확률값 배열로 부터 하나의 예측값을 구하기 위해 이들 5개 각 class별 확률 중에서 가장 큰 값을 가지는 위치 (indices of maximum value) 의 class 를 모델이 예측한 class 라고 정의해보겠습니다.  

np.argmax(pred_proba, axis=1) 은 배열 내의 각 관측치 별 (axis = 1) 로 가장 큰 확률값의 위치의 인덱스를 반환합니다.  가령, 위의 pred_proba 의 첫번째 관측치의 5개 class 별 속할 확률은 [0., 0., 0.2, 0.8, 0.] 의 배열로서, 확률 0.8 이 가장 큰 값이므로 위치 인덱스 '3'을 반환하였습니다.

 

## positional index for maximum probability
pred_idx = np.argmax(pred_proba, axis=1)
pred_idx
[Out]
array([3, 0, 2, 2, 4, 1])

 

 

(2) np.vectorize() 와 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

 

위의 (1)번에서 구한 확률 최대값의 위치 인덱스 가지고, 이번에는 아래의 'class_map_dict'와 같이 {키: 값} 쌍 사전의 '키(key)'를 기준으로 매핑을 해서, 다중분류 모델의 예측값을 'class 이름'으로 변환을 해보겠습니다.

 

## dictionary with pairs of {index_max_proba: class_name}
class_map_dict = {
    0: 'noraml', 
    1: 'class01', 
    2: 'class02', 
    3: 'class03',
    4: 'class04'
}

class_map_dict
[Out]
{0: 'noraml', 1: 'class01', 2: 'class02', 3: 'class03', 4: 'class04'}

 

 

 

이때 dict.get(key) 를 유용하게 사용할 수 있습니다. dict.get(key) 메소드는 사전(dict)의 키에 쌍으로 대응하는 값을 반환해줍니다. 따라서 바로 위에서 정의해준 'class_map_dict'의 키 값을 넣어주면, 각 키에 해당하는 'normal'~'class04' 의 사전 값을 반환해줍니다.

 

## get() returns the value for the specified key if key is in dict.
class_map_dict.get(pred_idx[0])
[Out]
'class03'


class_map_dict.get(0)
[Out]
'noraml'

 

 

사전의 (키: 값)을 매핑하려는 배열 내 원소가 많을 경우, np.vectorize() 메소드를 이용하면 매우 편리하고 또 빠르게 사전의 (키: 값)을 매핑을 해서 배열의 값을 변환할 수 있습니다. 아래 예에서는 'class_map_dict' 의 (키: 값) 사전을 사용해서 'pred_idx'의 확률 최대값 위치 인덱스 배열을 'pred_cls' 의 예측한 클래스(레이블) 이름('normal'~'class04')으로 변환해주었습니다.

 

np.vectorize() 는 numpy의 broadcasting 규칙을 사용해서 매핑을 하므로 코드가 깔끔하고, for loop을 사용하지 않으므로 원소가 많은 배열을 처리해야 할 경우 빠릅니다.

 

## vectorization of dict.get(array_idx) for all elements of array
pred_cls = np.vectorize(class_map_dict.get)(pred_idx)

pred_cls
[Out]
array(['class03', 'noraml', 'class02', 'class02', 'class04', 'class01'],
      dtype='<U7')
      

* np.vectorize() reference: numpy.org/doc/stable/reference/generated/numpy.vectorize.html

 

 

 

(3) for loop 과 dict.get() 을 사용해서 최대값 위치 인덱스와 분류 레이블을 매핑하기

 

만약 위의 (2)번 처럼 np.vectorize() 메소드를 사용하지 않는다면, 아래처럼 for loop 사용해서 확률 최대값 위치 인덱스의 개수 만큼 순환 반복을 하면서 dict.get() 함수를 적용해주어야 합니다. 위의 (2)번 대비 코드도 길고, 또 대상 배열이 클 경우 시간도 더 오래 걸리므로 np.vectorize() 사용을 권합니다.

 

## manually using for loop
pred_cls_mat = np.empty(pred_idx.shape, dtype='object')

for i in range(len(pred_idx)):
    pred_cls_mat[i] = class_map_dict.get(pred_idx[i])
    
pred_cls_mat
[Out]
array(['class03', 'noraml', 'class02', 'class02', 'class04', 'class01'],
      dtype=object)

 

 

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

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

 

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
,
이번 포스팅에서는 Python Numpy 의 배열(array)을 특정 형상(shape)으로 변형할 때 빈 자리를 '0'이나 다른 값으로 채우는 2가지 방법을 소개하겠습니다.

1. numpy.pad() 함수를 사용하여 배열(array)을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기
2. tensorflow.keras.preprocessing.sequence.pad_sequence() 함수를 사용하여 배열의 원소 개수가 다른 Ragged array 를 특정 형상의 배열로 바꾸면서 빈자리를 '0'으로 채우기


1. numpy 배열을 특정 형상의 배열로 변형할 때 빈자리를 '0'으로 채우기 (padding)

    : numpy.pad() 함수



먼저, numpy 라이브러리를 importing 하고, 예제로 사용할 2 by 3 의 간단한 2차원 배열(array)을 만들어보겠습니다.




import numpy as np


x = np.array([[1, 2, 3],
                 [7, 2, 5]])
print(x)

array([[1, 2, 3],
          [7, 2, 5]])




위의 2 by 3 의 2차원 배열 x 의 위, 아래에 1개씩의 행을 추가하고, 왼쪽, 오른쪽에 1개씩의 열을 추가하여 4 by 5 의 2차원 배열을 만들되, 새로 추가되는 행과 열의 자리는 '0'으로 채워넣기(padding)를 numpy.pad() 함수를 사용하여 해보겠습니다.

numpy.pad(array, pad_width, mode='constant', **kwargs)


# np.pad(x, (1, 1))

np.pad(x, (1, 1),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0]

    [0, 1, 2, 3, 0],     [0, 7, 2, 5, 0],     [0, 0, 0, 0, 0]])




만약 위의 행 1개 추가, 왼쪽 열 1개 추가, 아래쪽 행 2개 추가, 오른쪽 열 2개를 추가하고 싶다면 pad_width 매개변수에 (1, 2) 를 설정해주면 됩니다.


np.pad(x, (1, 2),
       mode='constant',
       constant_values=0)


array([[0, 0, 0, 0, 0, 0],
       [0, 1, 2, 3, 0, 0],
       [0, 7, 2, 5, 0, 0],
       [0, 0, 0, 0, 0, 0],
       [0, 0, 0, 0, 0, 0]])

 



np.pad() 메소드를 사용하지 않고, 아래처럼 numpy.zeros() 로 원하는 형상(shape)대로 모두 '0'으로 채워진 배열을 먼저 만들어놓고, indexing을 사용하여 왼쪽, 위쪽, 오른쪽, 아래쪽에 행과 열을 1개씩 비워놓을 수 있는 위치에 기존 배열을 삽입하여 np.pad() 메소드를 사용했을 때와 동일한 결과를 얻을 수도 있습니다. 이때는 새로 만들어지는 배열 z의 형상(shape)과 기존 배열 x를 채워넣을 위치의 indexing에 신경을 써주어야 하므로 조금 신경이 쓰이는 편이기는 합니다. (위의 np.pad() 와 일처리 순서가 정 반대라고 생각하면 됩니다.)


z = np.zeros((4, 5))
print(z)

[[0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0.]]


z[1:-1, 1:-1] = x
print(z)
[[0. 0. 0. 0. 0.] [0. 1. 2. 3. 0.] [0. 7. 2. 5. 0.] [0. 0. 0. 0. 0.]]

 



np.pad() 함수에 몇가지 재미있는 옵션을 마저 소개하겠습니다.
constant_values 를 설정해주면 '0' 대신에 원하는 다른 특정 상수 값으로 빈 자리를 채워넣을 수 있습니다. '0' 대신에 '-1'을 한번 채워볼까요?


np.pad(x, (1, 1), constant_values=-1)


array([[-1, -1, -1, -1, -1],
       [-1,  1,  2,  3, -1],
       [-1,  7,  2,  5, -1],
       [-1, -1, -1, -1, -1]])




빈 자리 채워넣기하는 방법(mode)에는 항상 똑같은 상수('constant', default) 값을 채워넣는 방법 외에도 'edge', 'linear_ramp', 'maximum', 'mean', 'median', reflect', 'symmetric', 'wrap', 'empty' 등의 다양한 mode 옵션을 제공합니다. 이들 중에서 위의 예시에서 사용한 'constant' 이외에 'edge', 'maximum', 'wrap' 의 mode 옵션을 사용하여 채워넣기 padding을 해보겠습니다. (아래 결과에서 빨간색으로 표시한 부분이 padding된 부분입니다.)


  • mode = 'edge' : 가장 변두리의 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='edge')


array([[1, 1, 2, 3, 3], [1, 1, 2, 3, 3], [7, 7, 2, 5, 5], [7, 7, 2, 5, 5]])

 


  • mode = 'maximum' : 행과 열의 가장 큰 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='maximum')


array([[7, 7, 2, 5, 7],
       [3, 1, 2, 3, 3],
       [7, 7, 2, 5, 7],
       [7, 7, 2, 5, 7]])



  • mode = 'wrap' : 행과 열의 반대편 끝에 있는 원소 값으로 빈 곳 채우기


np.pad(x, (1, 1), mode='wrap')


array([[5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1],
       [5, 7, 2, 5, 7],
       [3, 1, 2, 3, 1]])


* Reference: https://numpy.org/doc/stable/reference/generated/numpy.pad.html



  2. 원소 개소가 다른 Ragged array를 특정 형상의 배열로 바꿀 때 빈자리를 '0'으로 채우기

     : tensorflow.keras.preprocessing.sequence.pad_sequence() 함수


위의 np.pad() 함수의 경우 변경하기 전의 원래 배열이 (m by n) 형상인 고정된 차원의 배열을 대상으로 채워넣기를 하였습니다. 두번째로 소개하려는 keras의 sequence.pad_sequence() 함수는 각 행의 원소 개수가 다른 Ragged array(?) 를 대상으로 특정 (j by k) 형상의 고정된 배열로 바꾸려고 할 때 빈 자리를 '0'으로 채워넣는데 사용할 수 있는 차이가 있습니다.


아래의 예를 보면 원소 개수가 1개, 2개, 3개, 4개로서 들쭉날쭉함을 알 수 있습니다. (list를 원소로 가지고 있고, data type 이 object 이네요.)



x2 = np.array([[1], [2, 3], [4, 5, 6], [7, 8, 9, 10]])
display(x2)


array([list([1]), 
      list([2, 3]),
      list([4, 5, 6]),
      list([7, 8, 9, 10])], dtype=object)

 



TensorFlow와 Keras의 tf.keras.preprocessing.sequence() 메소드를 importing 해보겠습니다.



import tensorflow as tf
from tensorflow.keras.preprocessing import sequence

print(tf.__version__)

2.3.0

 



이제 Keras의 pad_sequences() 함수를 사용하여 가장 많은 원소를 가진 행에 맞추어서 (4 by 4) 형상의 배열로 바꾸고, 왼쪽의 빈자리는 '0'으로 채워넣기(padding)를 해보겠습니다.

tf.keras.preprocessing.sequence.pad_sequences(
    sequences, maxlen=None, dtype='int32',
    padding='pre', truncating='pre',
    value=0.0
)

이렇게 (4 by 4) 형상으로 해서 빈자리는 '0'으로 채워주고 나니 각 행의 원소 개수가 모두 4개로서 배열다운 배열이 되었습니다. padding을 해주는 위치의 기본 설정값은 padding='pre' 로서 앞쪽(왼쪽)에 '0'을 채워줍니다. (value=0 이 기본 설정값으로서 '0' 값으로 채워줌)


sequence.pad_sequences(x2) # default: padding='pre', value=0


array([[ 0,  0,  0,  1],
       [ 0,  0,  2,  3],
       [ 0,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



만약 padding을 해주는 위치를 뒤쪽(오른쪽)으로 하고 싶다면 padding='post' 로 매개변수 설정을 바꿔주면 됩니다.


sequence.pad_sequences(x2, padding='post')


array([[ 1,  0,  0,  0],
       [ 2,  3,  0,  0],
       [ 4,  5,  6,  0],
       [ 7,  8,  9, 10]], dtype=int32)

 



물론 빈 곳 채워넣기(padding)하는 값을 '0'이 아니라 다른 값으로 할 수도 있습니다. '-1'을 사용(value=-1)해서 앞쪽에 빈 곳을 채워넣기해보겠습니다.



sequence.pad_sequences(x2, padding='pre', value=-1)


array([[-1, -1, -1,  1],
       [-1, -1,  2,  3],
       [-1,  4,  5,  6],
       [ 7,  8,  9, 10]], dtype=int32)

 



maxlen 매개변수값을 별도로 설정해주지 않으면 배열 내 행 중에서 가장 많은 원소를 가진 행을 기준으로 maxlen 이 자동으로 정해지는데요, 이를 사용자가 직접 설정해줄 수도 있습니다. 아래의 예에서 maxlen=5 로 설정해주면 (4 by 5) 의 padding 된 배열이 생성됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=5)


array([[ 0,  0,  0,  0,  1],
       [ 0,  0,  0,  2,  3],
       [ 0,  0,  4,  5,  6],
       [ 0,  7,  8,  9, 10]], dtype=int32)




아래 예에서처럼 원래의 배열 x2 의 최대길이는 4인데 maxlen=3 으로 값을 설정하게 되면 4-3=1 개의 길이만큼의 원소 값들을 잘라내기(truncating) 해주어야 합니다. 이때 truncating='post' 라고 설정해주면 뒤쪽(오른쪽)을 기준으로 '1'개의 값들을 잘라내주고, 앞쪽을 기준으로 비어있는 곳에는 '0'의 값을 채워주게 됩니다.


sequence.pad_sequences(x2, padding='pre', value=0, maxlen=3, truncating='post')


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



* Reference: https://www.tensorflow.org/api_docs/python/tf/keras/preprocessing/sequence/pad_sequences

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

728x90
반응형
Posted by Rfriend
,