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
,
이번 포스팅에서는 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
,

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

 

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

 

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

 

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

 

 

 

 

  (1) 1부터 5까지의 정수 모집단에서 3개의 균등확률표본을 복원추출 하기
  (Generate a uniform random sample from np.arange(5) of size 3)

 

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

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

 



import numpy as np
 
np.random.choice(5, 3) # with replacement
Out[3]: array([4, 0, 0])
 

 

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

 

 

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

 



np.random.choice(5, 3) # sampled with different values
Out[4]: array([2, 0, 0])
 

 

 

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

 



np.random.choice(5, 10) # with replacement
Out[5]: array([2, 2, 4, 2, 4, 0, 0, 4, 3, 2])
 

 

 

 

 (2) 1~5의 정수 모집단에서 비균등 확률 p 로 3개 원소 임의 표본 복원추출 하기
    (Generate a non-uniform random sample from np.arange(5) of size 3:)

 

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

 



p = [0.1, 0, 0.3, 0.6, 0]
np.random.choice(5, 3, p=p)
Out[7]: array([2, 3, 3])
 

 

 

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

 



# ValueError: 'a' and 'p' must have same size
p = [0.1, 0, 0.3]           # size 3
np.random.choice(5, 3, p=p) # size 5


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

 

 

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

 



# ValueError: probabilities do not sum to 1
p = [0.4, 0, 0.3, 0.6, 0]   # sum to 1.3 (not 1)
np.random.choice(5, 3, p=p)


ValueError: probabilities do not sum to 1




# ValueError: probabilities are not non-negative
p = [-0.4, 0, 0.3, 1.6, 0]   
np.random.choice(5, 3, p=p)


ValueError: probabilities are not non-negative

 

 

 

  (3) 1~5 정수 모집단에서 3개의 균등확률표본을 비복원추출(non-replacement) 하기
  (Generate a uniform random sample from np.arange(5) of size 3 
   without replacement)

 

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

 



# Generate a uniform random sample from np.arange(5) of size 3 without replacement:
np.random.choice(5, 3, replace=False)
Out[11]: array([3, 2, 1])
 

 

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

 

 

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

 



# ValueError: Cannot take a larger sample than population when 'replace=False'
np.random.choice(5, 10, replace=False)


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

 

 

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

 



# Generate a non-uniform random sample from np.arange(5) of size 3 without replacement:
p = [0.1, 0, 0.3, 0.6, 0]
np.random.choice(5, 3, replace=False, p=p)
Out[13]: array([3, 0, 2])
 

 

 

 

  (4) 정수 대신에 임의의 배열처럼 생긴 객체의 원소를 확률추출하기
  (Any of the above can be repeated with an arbitrary array-like instead of just integers.)

 

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

 



# Any of the above can be repeated with an arbitrary array-like instead of just integers.
fruits = ['apple', 'banana', 'cherries', 'durian', 'grapes', 'lemon', 'mango']
p = [0.1, 0, 0.2, 0.5, 0.1, 0.05, 0.05]
np.random.choice(fruits, 3, p=p, replace=False)
Out[14]: array(['cherries', 'lemon', 'durian'], dtype='<U8')
 

 

 

 

  (5) 초기값 설정을 통한 재현가능성 확보
  (setting seed number for reproducibility)

 

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

 



# set seed number for reproducibility
np.random.seed(1004)
np.random.choice(5, 3)
Out[15]: array([2, 3, 3])


np.random.seed(1004)
np.random.choice(5, 3)
Out[16]: array([2, 3, 3])

 

 

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

 

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

이번 포스팅이 도움이 되었다면 아래의 '공감~

'를 꾹 눌러주세요. :-)

 

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

 

 

728x90
반응형
Posted by Rfriend
,

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


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

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

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

    : 의사결정 경계(Decision Boundary)

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


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


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


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



#%% import libraries

import pandas as pd

import numpy as np

import matplotlib.pyplot as plt

import seaborn as sns


#%% iris dataset

iris = sns.load_dataset('iris')

iris.info()

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

RangeIndex: 150 entries, 0 to 149

Data columns (total 5 columns):

 #   Column        Non-Null Count  Dtype  

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

 0   sepal_length  150 non-null    float64

 1   sepal_width   150 non-null    float64

 2   petal_length  150 non-null    float64

 3   petal_width   150 non-null    float64

 4   species       150 non-null    object 

dtypes: float64(4), object(1)

memory usage: 6.0+ KB



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

Out[1]: 

species

setosa        50

versicolor    50

virginica     50

dtype: int64



#%% scatter plot by 'species'

sns.scatterplot(x='petal_length', 

                y='petal_width', 

                hue='species', 

                style='species', 

                s=100, 

                data=iris)

plt.show()



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


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



#%% Classification using Random Forest

from sklearn.ensemble import RandomForestClassifier


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

y = iris['species']

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


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

rfc.fit(X, y)

 



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

      Random Forest 분류 모델로 예측


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



#%% predict

pred = rfc.predict(X)

print(pred == y)

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

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

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

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

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

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

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

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

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

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

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

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

  True  True  True  True  True  True]

 



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

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



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

h = 0.01

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

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

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

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


XY.shape

Out[4]: (158600, 2)


# predict

pred_cls = rfc.predict(XY)


# align the shape of Z with xx

Z = pred_cls.reshape(xx.shape)

 




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

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


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



# Random Forest Classifier: Decision Boundary

plt.contourf(xx, yy, Z)

plt.axis('off')




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



plt.contourf(xx, yy, Z)

plt.axis('off')


sns.scatterplot(x='petal_length', 

                y='petal_width', 

                hue='species', 

                style='species', 

                s=100, 

                data=iris)

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

plt.show()



 



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



plt.contourf(xx, yy, Z)

plt.axis('off')


# or plot data points using matplotlib and for loop

N = 50

CLS_NUM = 3

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

for i in range(CLS_NUM):

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


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



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

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



728x90
반응형
Posted by Rfriend
,

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


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

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


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


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





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


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



import numpy as np


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

print(A)

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


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

print(B)

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


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




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

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


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


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

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


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

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


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

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


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

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



for i, id in enumerate(idx):

    A[id] += B[i]


print(A)

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

 




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


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



import numpy as np


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

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

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


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


print(A)

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

 



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

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


728x90
반응형
Posted by Rfriend
,

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




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



import numpy as np

import pandas as pd


# Make a DataFrame with missing values

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

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

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

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

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

 

df

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




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


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

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



missing_fill_val = {'a': 0, 

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

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

                    'd': 'Unknown', 

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


print(missing_fill_val)

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

 



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



df2 = df.fillna(missing_fill_val)


df2


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

 



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



df.fillna(missing_fill_val, inplace=True)

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

 



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

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



728x90
반응형
Posted by Rfriend
,