Tools (123)

반응형


다른 컴퓨터로 conda 가상환경 옮기는 방법


참고

conda-cheatsheet.pdf


기존에 사용하던 컴퓨터 A 에서 컴퓨터 B 로 conda 가상환경을 옮겨야할 때가 있다. 


컴퓨터 A 에서 해야할 일 


1. 가상환경 켜기


source activate [이름]


2. 가상환경이 켜진 상태에서 아래 명령어로 dependency 를 export 할수 있다.


conda env export > environment.yaml

environment.yaml 파일을 열어보면 아래와 같이 잘 export 되었다는 것을 확인할 수 있다. 



3. 현재 환경의 python 버전 체크


현재 사용하고 있는 가상환경에서 사용하고 있는 python version 을 체크한다. 


python --version
Python 3.6.6


컴퓨터 B 에서 해야할 일 


Requirements! 

  • 컴퓨터 B 에서는 컴퓨터 A 에서와 같은 anaconda (python 2 또는 python3) 를 사용해야한다. 만약 anaconda 버전이 하위버전이면 잘 안돌아갈 수도 있을듯 하다. 

4. 가상환경 생성 


conda create --name [이름] python=3.6


5. prefix 변경


environment.yaml 을 열고 원하는 경우, 가상 환경의 이름을 바꾸고, prefix 를 경로에 맞게 바꾸어준다. 예를 들어, 


prefix: C:\Users\[사용자이름] \Anaconda3\envs\[가상환경이름]


5. yaml 파일을 통한 가상환경 생성 


conda env create --file environment.yaml


이 때, 


Solving environment: failed

ResolvePackageNotFound:


에러가 뜰 수 있다. 이것은 A 컴퓨터에서 설치된 라이브러리가 B 컴퓨터에서 설치가 불가능한 것인데, 컴퓨터의 운영체제가 다른 경우에 발생하는 것으로 보인다. 해결 방법은 수동으로 ResoevePackageNotFound 에서 출력된 리스트를 environment.yaml 파일에서 지운 후, 다시 시도하면 된다. (참고)



6. 주피터를 사용하는 경우, 커널에 가상환경 등록


source activate myenv python -m ipykernel install --user --name myenv --display-name "Python (myenv)"


반응형
반응형


Jupyter 유용한 확장기능 - lab_black


설치


pip install nb_black


사용법


notebook 사용자의 경우, 첫번째 셀에 아래 코드 실행 


%load_ext nb_black


lab 사용자의 경우, 첫번째 셀에 아래 코드 실행 


%load_ext lab_black


이후 코드를 실행하면, 자동으로 black format 으로 포매팅이 되는 것을 볼 수 있다. 따로 command 를 이용해서 formatting 을 하지 않아도 실행하는 즉시 formatting 이 되기 때문에 매우 유용하다!


링크 

https://github.com/dnanhkhoa/nb_black?source=your_stories_page

반응형
반응형

Early Stopping 이란 무엇인가? 


딥러닝을 비롯한 머신러닝 모델의 한 가지 중요한 딜레마는 다음과 같다. 


너무 많은 Epoch 은 overfitting 을 일으킨다. 하지만 너무 적은 Epoch 은 underfitting 을 일으킨다. 


이런 상황에서 Epoch 을 어떻게 설정해야하는가? 

Epoch 을 정하는데 많이 사용되는 Early stopping 은 무조건 Epoch 을 많이 돌린 후, 특정 시점에서 멈추는 것이다. 


그 특정시점을 어떻게 정하느냐가 Early stopping 의 핵심이라고 할 수 있다. 일반적으로 hold-out validation set 에서의 성능이 더이상 증가하지 않을 때 학습을 중지시키게 된다. 본 포스팅에서는 Keras 를 이용하여 Early stopping 을 구현하는 법과 성능이 더 이상 증가하지 않는다는 것은 어떤 기준으로 정하는 것인지를 중점으로 정리해보고자 한다.  


Early Stopping in Keras


Keras 의 Early stopping 을 구현하는 Early stopping 함수를 통해 구현할 수 있다. 


from keras.callbacks import EarlyStopping


Earlystopping 클래스의 구성 요소

  • Performance measure: 어떤 성능을 monitoring 할 것인가?
  • Trigger: 언제 training 을 멈출 것인가?

Earlystopping 객체는 초기화될 때 두개의 요소를 정의하게 된다. 


아래와 같이 지정하면 validation set 의 loss 를 monitoring 한다는 뜻이다. 


es = EarlyStopping(monitor='val_loss')


만약 performance measure가 최소화 시켜야하는 것이면 mode를 min 으로, 최대화 시켜야하는 것이면 mode를 max로 지정한다. loss 의 경우, 최소화 시키는 방향으로 training 이 진행되므로 min 을 지정한다. 


es = EarlyStopping(monitor='val_loss', mode='min')


mode 의 default 는 auto 인데, 이는 keras 에서 알아서 min, max 를 선택하게 된다. 여기까지가 가장 기본적인 Early stopping 의 사용법이다. performance measure를 정의하고, 이것을 최대화 할지, 최소화 할지를 지정하는 것이다. 그러면 keras 에서 알아서 적절한 epoch 에서 training 을 멈춘다. verbose=1 로 지정하면, 언제 keras 에서 training 을 멈추었는지를 화면에 출력할 수 있다. 


성능이 증가하지 않는다고, 그 순간 바로 멈추는 것은 효과적이지않을 수 있다. patience 는 성능이 증가하지 않는 epoch 을 몇 번이나 허용할 것인가를 정의한다. partience 는 다소 주관적인 기준이다. 사용한 데이터와 모델의 설계에 따라 최적의 값이 바뀔 수 있다. 


es = EarlyStopping(monitor='val_loss', mode='min', verbose=1, patience=50)


만약 performance measure 를 practical 하게 설정한 경우 성능의 증가의 기준을 직접 정의할 수 있다. 예를 들어 아래 코드는 validation accuracy 가 1% 증가하지 않는 경우, 성능의 증가가 없다고 정의한다. 


특정값에 도달했을 때, 더 이상 training 이 필요하지 않은 경우가 있다. 이 경우 baseline 파라미터를 통해 정의할 수 있다. 


es = EarlyStopping(monitor='val_loss', mode='min', baseline=0.4)


최종적으로 mode.fit 함수의 callback 으로 early stopping 객체를 넣어주면 early stopping 을 적용할 수 있다. 


hist = model.fit(train_x, train_y, nb_epoch=10,  

                 batch_size=10, verbose=2, validation_split=0.2,   

                 callbacks=[early_stopping])  


Model Choice


Early stopping 객체에 의해 트레이닝이 중지되었을 때, 그 상태는 이전 모델에 비해 일반적으로 validation error 가 높은 상태일 것이다. 따라서, Earlystopping 을 하는 것은 특정 시점에 모델의 트레이닝을 멈춤으로써, 모델의 validation error 가 더 이상 낮아지지 않도록 조절할 수는 있겠지만, 중지된 상태가 최고의 모델은 아닐 것이다. 따라서 가장 validation performance 가 좋은 모델을 저장하는 것이 필요한데, keras 에서는 이를 위해 ModelCheckpoint 라고 하는 객체를 존재한다. 이 객체는 validation error 를 모니터링하면서, 이전 epoch 에 비해 validation performance 가 좋은 경우, 무조건 이 때의 parameter 들을 저장한다. 이를 통해 트레이닝이 중지되었을 때, 가장 validation performance 가 높았던 모델을 반환할 수 있다. 


from keras.callbacks import ModelCheckpoint


mc = ModelCheckpoint('best_model.h5', monitor='val_loss', mode='min', save_best_only=True)


위 ModelCheckpoint instance를 callbacks 파라미터에 넣어줌으로써, 가장 validation performance 가 좋았던 모델을 저장할 수 있게된다.


hist = model.fit(train_x, train_y, nb_epoch=10,  

                 batch_size=10, verbose=2, validation_split=0.2,   

                 callbacks=[early_stopping, mc])  

참고

https://machinelearningmastery.com/how-to-stop-training-deep-neural-networks-at-the-right-time-using-early-stopping/

https://machinelearningmastery.com/early-stopping-to-avoid-overtraining-neural-network-models/

반응형
반응형

Pandas 에서 반복을 효율적으로 처리하는 방법


Pandas 를 통해 데이터 프로세싱을 할 때 종종 해야할일은 행에 반복적으로 접근을 하면서 그 값을 조작하는 일이다. 예를 들어, missing value 가 0 으로 코딩이 되어있는데, 이를 다른 값으로 바꾸고 싶을 경우 또는 A 컬럼의 값이 missing 일 때, B 컬럼의 값을 수정하고 싶은 경우 등이 있다. 이러한 작업을 하기 위해서는 모든 행을 조회 하면서 값을 조회하고 수정하는 일이 필요하다. 이번 포스팅에서는 이러한 반복작업이 필요한 상황에서 어떤 방법이 가장 효율적일지에 대해 정리해보려고한다.


사용할 데이터

diabetes.csv


1) pd.iterrows()


가장 기본적이고 많이 사용하는 방법이 iterrows 함수를 이용하는 것이다. 하지만 iterrows 함수는 다른 방법에 비해 느린 편이다. 

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline

diabetes = pd.read_csv("diabetes.csv")
diabetes.head()


PregnanciesGlucoseBloodPressureSkinThicknessInsulinBMIDiabetesPedigreeFunctionAgeOutcome
061487235033.60.627501
11856629026.60.351310
28183640023.30.672321
318966239428.10.167210
40137403516843.12.288331


missing value 가 0 으로 코딩이 되어있는데, 이를 nan 으로 바꾸는 코드를 iterrows 를 이용해서 짜보자. 

def fix_missing(df, col):
    for i, row in df.iterrows():
        val = row[col]
        if val == 0:
            df.loc[i, col] = np.nan

%timeit fix_missing(diabetes, "SkinThickness")


33.9 ms ± 1.76 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)



2) pd.loc[]/pd.iloc[]


두 번째 방법은 index 를 통해 for 문을 돌면서, loc 또는 iloc 함수를 이용해 dataframe의 row에 접근하는 방법이다. 

def fix_missing2(df, col):
    for i in df.index:
        val = df.loc[i, col]
        if val == 0 :
            df.loc[i, col] = np.nan

%timeit fix_missing2(diabetes, "Insulin")


9.54 ms ± 130 µs per loop (mean ± std. dev. of 7 runs, 1 loop each)


1) iterrow 방법에 비해 약 3배 빨라졌다는 것을 알 수 있다. 따라서 iterrows 가 익숙하다고 하더라도 다른 방법으로 바꾸는 것이 같은 작업을 더 빠르게 실행할 수 있어 효율적이다.  


3) pd.get_value()/pd.set_value()


다음은 위 방법과 마찬가지로 index를 통해 for 문을 돌면서 get_value 와 set_value 함수를 이용하는 방법이다. 2) 방법이 내부적으로 get_value, set_value를 호출하는 것이기 때문에 3) 이를 직접적으로 호출하는 방법이므로 더욱 빠르다. 

def fix_missing3(df, col):
    for i in df.index:
        val = df.get_value(i, col)
        if val == 0:
            df.set_value(i, col, np.nan)

%timeit fix_missing3(diabetes, "BMI")


3.65 ms ± 31.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)


2)에 비해 3배 정도 빨라졌으며, 1)에 비해서는 거의 10배 정도의 속도차이가 난다. 


4) pd.apply()


네 번째 방법은 apply 를 이용하는 것이다. apply 를 이용하는 것은 특별한 형태의 function 을 필요로 하는데 (이를 helper function 이라고도 한다), 이것은 Series 혹은 Dataframe의 각 원소마다 적용시킬 함수이다. 

def fix_missing4(x):
    if x == 0 : 
        return -999
    else: return x
    
%timeit diabetes.Age.apply(fix_missing4)
483 µs ± 3.89 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

3) 방법과 비교하여 3650/484 = 7.5 배 속도가 증가했다. apply 함수를 이용하는 것의 장점은 작업이 "비교적 간단할 때" 유용하다. 만약, 여러개의 column 에서 if 문을 적용하서 값을 다이나믹하게 바꾸어야하는 작업에 있어서는 apply 함수보다 for 문을 이용하는 방법이 더 적절할 수 있다. 

정리 
  • Default 방법으로 index 를 돌면서 set_value 와 get_value 를 호출하는 방법을 추천
  • 비교적 큰 데이터에셋에서 비교적 간단한 작업을 할 때, apply 함수가 가장 효율적


참고

https://medium.com/@rtjeannier/pandas-101-cont-9d061cb73bfc

반응형
반응형


R - Windows 에서 source 로부터 패키지 설치하기 



devtools 를 통해 최신 개발 버전의 tidyr 을 설치 



devtools::install_github("tidyverse/tidyr")

Downloading GitHub repo tidyverse/tidyr@master

Error: Could not find tools necessary to compile a package


그냥 설치하면 위와 같은 에러가 나오면서 설치되지 않음 


아래 옵션 추가함으로써 무엇이 잘못되었는지, 어떤 dependencies 를 설치할 때 오류가 발생한 것인지 확인 가능

options(buildtools.check = function(action) TRUE )


다시 아래 코드 실행 
devtools::install_github("tidyverse/tidyr")

These packages have more recent versions available.
Which would you like to update?

1:   rlang    (0.3.4 -> 6a232c019...) [GitHub]
2:   ellipsis (0.1.0 -> d8bf8a34e...) [GitHub]
3:   CRAN packages only
4:   All
5:   None

4. All 을 입력해줌 

WARNING: Rtools is required to build R packages, but is not currently installed.

Please download and install Rtools 3.5 from http://cran.r-project.org/bin/windows/Rtools/.


Rtools 가 소스를 컴파일할 때 필요함 


RTools 설치 - 버전에 맞는 R tools 를 설치함

https://cran.r-project.org/bin/windows/Rtools/


Rtools 폴더를 환경변수 걸기 

예를 들어 C:/Rtools 를 환경변수로 잡음


Permission denied error 가 뜰 수 있는데 공용폴더로 lib path 를 지정하면 된다.

아래처럼 공용 폴더에 install_github 를 통해 패키지를 설치할 수 있음 

with_libpaths(new = "C:/Program Files/R/R-3.5.3/library", install_github('tidyverse/tidyr')) 


결국 소스를 컴파일 하면서 잘 설치가 되는 것을 확인할 수 있음 


C:/Rtools/mingw_64/bin/g++  -I"C:/PROGRA~1/R/R-35~1.3/include" -DNDEBUG  -I"C:/Users/JuYoungAhn/Documents/R/win-library/3.5/Rcpp/include"        -O2 -Wall  -mtune=generic -c RcppExports.cpp -o RcppExports.o

C:/Rtools/mingw_64/bin/g++  -I"C:/PROGRA~1/R/R-35~1.3/include" -DNDEBUG  -I"C:/Users/JuYoungAhn/Documents/R/win-library/3.5/Rcpp/include"        -O2 -Wall  -mtune=generic -c fill.cpp -o fill.o

C:/Rtools/mingw_64/bin/g++  -I"C:/PROGRA~1/R/R-35~1.3/include" -DNDEBUG  -I"C:/Users/JuYoungAhn/Documents/R/win-library/3.5/Rcpp/include"        -O2 -Wall  -mtune=generic -c melt.cpp -o melt.o

C:/Rtools/mingw_64/bin/g++  -I"C:/PROGRA~1/R/R-35~1.3/include" -DNDEBUG  -I"C:/Users/JuYoungAhn/Documents/R/win-library/3.5/Rcpp/include"        -O2 -Wall  -mtune=generic -c simplifyPieces.cpp -o simplifyPieces.o


......


library("tidyr", lib.loc="C:/Program Files/R/R-3.5.3/")


이렇게 path 를 지정해주면 라이브러를 로딩할 수 있음


반응형
반응형

R 을 더욱 효율적으로 사용하는 방법 3가지



프로그래밍에서의 효율성 


프로그래밍에서의 효율성이란 아래와 같이 두 가지로 나누어볼 수 있다. 



- Algorithmic efficiency

: 컴퓨터가 어떠한 일을 얼마나 빠르게 수행할 수 있을지에 관한 것


- Programmer productivity

: 단위 시간동안 사람이 할 수 있는 일의 양 


부연 설명을 하자면, 만약 어떠한 코드를 R 이 아니라 C로 구현을 하면 100배 빠르다고 하자, 하지만 그 코드를 짜는데 100배 넘는 시간이 들고, 그것이 여러번 사용되지 않고 딱 한 번 사용되고 버려지는 코드라면 그 일은 아무런 의미가 없게 된다.



R 의 특수성


우선 효율적인 R 프로그래밍을 알아보기 전에 R 언어의 특징을 알아보자. 


- R 언어는 무언가를 해결하기 위한 다양한 방법을 제공한다. 

그것은 장점이 되기도 하고 단점이 되기도 한다. 단점이란 명확한 '정답' 이 없어 헤멜 수 있다는 것이고, 장점은 다양한 접근법을 시도할 수 있기 때문에 개인의 창의성이 발휘될 수 있다는 것이다. R 에서는 모든것이 생성된 후에 변형될 수 있다라는 말처럼 (“anything can be modified after it is created” - 해들리 위컴) R 은 매우 유연한 언어이다. 


- R은 컴파일 언어가 아니다. 하지만 컴파일된 코드를 call 할 수 있다. 

따라서 코드를 컴파일링하는 작업을 하지 않아도 된다는 장점이 있다. 한 편, 최대한 C나 FORTRAN 으로 컴파일된 코드를 call 하면 그만큼 빠른 속도의 증가를 불러올 수 있다. 또한 R 은 함수형 언어이며, 객체지향언어이기 때문에 같은 작업을 하는 code 를 짧게 쓸 수 있다는 특징이 있다.



Algorithmic efficiency in R


R 은 컴파일된 코드를 call 할 수 있기 때문에 R 의 Algorithmic efficiency 를 향상시키기 위한  Golden-rule  C/Fortran 에 최대한 빠르게 접근하는 것이다. 그걸 어떻게 하는지에 관한 것은 Efficient R Programming 에서 자세히 다루고 있다. 본 포스팅에서는 Programmer efficiency 에 대해서 간단하게 다루어 보려고 한다. 



Programmer efficiency in R


R에는 하나의 문제를 해결하기 위한 다양한 해결법이 있다. 예를 들어, data frame 의 subsetting 에서도 data$x 도 쓸 수 있고 data[['x']] 도 쓸 수 있다. 하지만 '결과를 중시하는 코딩' 을 하는 경우, 어떠한 문제를 해결할 '가장 효율적인 방법' 이 무엇인지 알기만 하면 된다. 본 포스팅에서는 이 관점에서 어떻게하면 R 을 통해 결과를 빠르고 생산성 있게 작성할 수 있는지에 대해 간단하게 정리해보려고 한다. 



1. Conding convention 을 사용해보자 


Good coding style is like using correct punctuation. You can manage without

it, but it sure makes things easier to read. - Hadley Wickham


위 말처럼 좋은 코딩 스타일을 갖는 것은 구두점을 정확하게 사용하는 것과 같다. 구두점이 없더라도 문장을 읽을 수는 있지만 구두점이 있다면 문장을 더 쉽게 읽을 수 있게 된다. 특히 일관적인 코딩 스타일은 협업을 할 때 중요하다. 협업을 하지 않고, 혼자 작업을 하더라도 자신의 코드를 오랜만에 보았을 때, 코딩 스타일이 일관적이라면 더 쉽게 이해할 수 있다. 


R 에는 공식 스타일 가이드가 없다. 하지만 R 커뮤니티는 이미 성숙도가 높기 때문에 유명한 coding convention 이 존재한다. 


Bioconductor’s coding standards (https://bioconductor.org/developers/how-to/coding-style/)

- Hadley Wickham’s style guide (http://stat405.had.co.nz/r-style.html )

- Google’s R style guide (http://google-styleguide.googlecode.com/ svn/trunk/google-r-style.html )

- Colin Gillespie’s R style guide (http://csgillespie.wordpress.com/2010/ 11/23/r-style-guide/)

The State of Naming Conventions in R (by Rasmus Bååth)


참고하면 좋을 자료 

https://www.r-bloggers.com/%F0%9F%96%8A-r-coding-style-guide/ 



2. 반복 작업을 짧고, 간결하게 작성하자


R Apply Family


R 에서의 반복작업을 할 때, Apply 계열의 함수 (Apply family 라고 부르기도 한다.) 를 이용하면 좋다. Apply 계열의 함수는 함수의 인자로 함수를 넣어주는 특징을 갖고 있다. 인자로 들어온 함수를 데이터에 대해 여러번 적용 시켜서 결과를 반환하는 것이다. Apply 계열의 함수를 이용하면 1) 빠르고, 2) 읽기 쉽다. 


1. Apply 계열의 함수는 C 코드로 되어있다. 비록, for 문의 성능이 최근들어 좋아졌다고 하더라도, Apply 계열의 함수가 더 빠르다.


2. '어떻게' 하는지가 아니라 '무엇을' 하는지에 대해서 강조할 수 있다. R 의 목적은, 프로그래밍이 아니라, 데이터를 Interactive 하게 탐구하는 것이다. Apply 계열의 함수를 이용하는 것은 R 언어의 본질적 특징인 Functional Programming 의 원칙에 부합하는 코딩 스타일이라고 할 수 있다. 


이와 관련해서는 이전 포스팅에 정리한적이 있다. 또한 다양한 책과 블로그에서 Apply family 에 관하여 잘 정리가 되어있으니 참고하면 좋다. 또한 이러한 Apply 계열의 함수는 R 고유의 문법이 아니다. Python 에서도 이와 비슷하게 함수를 input 으로 받아들이는 함수들이 있다. Map, Filter, Reduce 가 그것이다. 이에 대해서도 이전 포스팅에 정리한 적이 있다. 따라서 이러한 문법에 익숙해 진다면 다양한 프로그래밍 언어를 사용할 때, 훨씬 생산성을 높일 수 있다. 


Overview

FunctionDescription
applyApply functions over array margins
byApply a function to a data frame split by factors
eapplyApply a function over values in an environment
lapplyApply a function over a list or vector
mapplyApply a function to multiple list or vector arguments
rapplyRecursively apply a function to a list
tapplyApply a function over a ragged array


Purrr 


Tidyverse 의 멤버중 하나인 Purrr 을 이용하면 반복작업을 Apply family 에 비해 더욱 직관적이고 쉽게 할 수 있다.


Purrr 패키지는 고양이의울음소리를 의미하는 purr 과 r을 합친 의미로, 위와 같이 귀여운 로고를 가졌다. 아래는 공식 홈페이지에서 purrr 에 대한 설명이다. 


purrr enhances R’s functional programming (FP) toolkit by providing a complete and consistent set of tools for working with functions and vectors. If you’ve never heard of FP before, the best place to start is the family of map() functions which allow you to replace many for loops with code that is both more succinct and easier to read. The best place to learn about the map() functions is the iteration chapter in R for data science. 


Purrr 에서 가장 기본적인 함수는 map() 이다. map 이 어떻게 사용되는 지를 간단하게 알아보자. 


df 에서 a,b,c,d 의 평균을 출력하는 작업을 해보자. 


mean(df$a)

mean(df$b)

mean(df$c)

mean(df$d)


이를 반복문을 사용하면 아래와 같이 할 수 있다. 


for var in c("a", "b", "c", "d") {

print(mean(var), trim = 0.5)

}


map 함수를 사용하면 아래와 같이 할 수 있다. 


map_dbl(df, mean, trim = 0.5)

#>      a      b      c      d 
#>  0.237 -0.218  0.254 -0.133

두 접근법의 차이는 무엇일까? 


for 문을 사용한 것은 how 에 관한 정보를 포함하고 있지만 map 함수는 what 에 집중한다. 즉, 이 코드에서 하고자하는 것은 평균을 내는 것이다. R 은 그 작업을 "어떻게" 하는지에는 관심이 없다. map 을 사용하면 하고자 하는 작업인 '평균내기' 에 집중한 코드를 작성할 수 있다. 이와 관련해서는 Hadley Wickham 의 강의를 참고하기 바란다. 



3. 효율적인 Data Carpentry 를 하자 


데이터 처리를 일컫는 수많은 말이 있다. clean, hack, manipulation, munge, refine, tidy 등이다. 이 과정 이후에 modeling 과 visualization 을 수행하게 된다. 데이터 처리는 실제 재미있고, 의미 있는 작업을 하기 전에 수행되는 dirty work 로 생각되어지기도 한다. 왜냐하면 시간만 있으면 그것을 할 수 있다는 것이 한 가지 이유일 것이다. 하지만 데이터를 아주 깔끔하게 정돈하고, 원하는 형태를 빠르고 정확하게 만들어내는 것은 필수적인 능력이다.  또한 데이터처리 과정을 추상화해서 재생산 가능한 (reproducible) 코드를 만드는 작업은 단순 노동과는 다른 높은 숙련도를 가져야만 할 수 있는 것이다. 따라서 이러한 사람들의 인식은 틀렸다고 할 수 있다. 또한 미국에는 데이터 처리만 해서 판매하는 회사도 존재하며, 이 회사의 가치는 매우 높다! 이러한 데이터 처리 과정에 대해 Efficient R Programming 의 저자이자 유명한 R 교육자인 Colin Gillespie 와 Robin Lovelace는 Data Carpentry 라는 이름을 붙였다. 


Colin Gillespie 와 Robin Lovelace 가 정리한 효율 적인 Data Carpentry 를 위한 5가지 팁은 아래와 같다. 


1. Time spent preparing your data at the beginning can save hours of frustration in the long run.

2. ‘Tidy data’ provides a concept for organising data and the package tidyr provides some functions for this work.

3. The data_frame class defined by the tibble package makes datasets efficient to print and easy to work with.

4. dplyr provides fast and intuitive data processing functions; data.table has unmatched speed for some data processing applications.

5. The %>% ‘pipe’ operator can help clarify complex data processing workflows.


Data carpentry 는 크게 두 가지로 나눌 수 있다.  


Tidying : Raw 데이터를 "Tidy data" 로 만드는 작업 

Transformation: "Tidy data" 를 원하는 형태로 만드는 작업 (subsetting, adding column, summarizing 등)


대표적으로, Tidying 을 위한 패키지는 tidyr, Transformation 을 위한 패키지는 dplyr, data.table 패키지가 있다. 이 과정에서 Tibble 이라는 데이터 프레임을 상속한 클래스를 사용하면 데이터를 print 하거나, 처리할 때 있어 더욱 효율적이며 magrittr 의 pipe (%>%) 는 더욱 읽기 쉽고 직관적인 코드를 만든다. R 기본 패키지에 비해 dplyr 를 사용했을 때의 장점에 대해서는 이전 포스팅에 정리한 적이 있다. 필자 의견으로는 한 번도 dplyr 를 사용하지 않은 사람은 있어도, 한 번만 dplyr 를 사용한 사람은 없다고 생각될 정도로 dplyr 를 사용했을 때 높은 생산성을 가질 수 있다. 


예를 들어, 특정 행을 subsetting 하는 코드를 작성한다고 해보자. 


flights 라는 data frame 에서 arr_delay 가 120 미만인 행을 골라낸다고 하면, R 기본 subsetting 방법으로 하면 아래와 같다. [] 안에 logical vector 를 넣는 subsetting 방법이다. 문제는, is.na 를 이용해 arr_delay 가 na 인 경우도 고려해야한다는 것이다. 


flights[(flights$arr_delay < 120) & !is.na(flights$arr_delay), ]

하지만 dplyr 를 사용하면, 이러한 기술적인 것과 상관없이 직관적으로 코드를 작성할 수 있다. 


filter(flights, arr_delay < 120


반응형
반응형

R 커뮤니티의 최신 유행을 Follow 하기 좋은 방법 


R 커뮤니티의 성숙은 매우 빠르게 이루어지고 있고, 그러한 주류에 속하기 위해서는 뉴스를 빠르게 습득해야할 필요가 있습니다. 이와 관련하여 이전 글에서 R과 데이터과학의 트렌드를 알 수 있는 블로그들을 소개했습니다 


하지만 더욱 빠르게 정보를 습득할 수 있는 방법은 트위터의 rstats 해쉬태그를 이용하는 것입니다.  


https://twitter.com/hashtag/rstats?src=hash&lang=en&lang=en

해당 트윗에 들어가면 실시간으로 R 커뮤니티에 올라오는 새로운 소식들을 만나볼 수 있습니다. 



블로그나, 최신 책을 이용하는 방법도 있지만, 트위터는 몇 분마다 한 번씩 글이 올라오니 가장 최신 정보를 생생하게 알 수 있다는 장점이 있어 좋은것 같네요.



이외에도 다양한 트위터들이 있으니 Follow 하면 도움이 될 것 같습니다.]


또 Reddit 의 Rstats 페이지도있습니다. (https://www.reddit.com/r/rstats/)

반응형
반응형
Object Oriented Programming in R

R 베이스타입

  • 모든 베이스 R 개체는 그 객체가 메모리에 저장되는 방법을 기술하는 C 구조가 있다.
  • 베이스 타입은 R 코어팀만 만들 수 있다. 실제 객체 시스템은 아니다.
  • typeof 함수는 R base type 을 반환한다. (vector, list, function, builtin)
typeof(c("a")) # character vector 
#> [1] "character"
typeof(mean) # 함수는 closure
#> [1] "closure"
typeof(sum) # 원시함수는 built in 
#> [1] "builtin"
typeof(abs) # builtin 
#> [1] "builtin"
typeof(pnorm) # closure 
#> [1] "closure"
typeof(array(c(1.1, 2.2, 3.3), 3)) 
#> [1] "double"
typeof(matrix(1:3, 3)) 
#> [1] "integer"
df <- data.frame(x = 1:10, y = letters[1:10]) # dataframe 의 base type 은 list 이다. 
typeof(df)
#> [1] "list"

S3

  • S3는 가장 단순한 OO 시스템이고, base와 stats 패키지에서 사용된 유일한 OO 시스템이다.
  • CRAN 에 있는 많은 패키지들에서 가장 공통적을 사용되는 시스템이다.
  • pryr 패키지의 otype 함수는 해당 객체가 base type 그 자체인지 어떤 base type 에 기반한 S3 인지 S4 등 인지를 출력한다.
library(pryr)
df <- data.frame(x = 1:10, y = letters[1:10])
otype(df) # data frame은 S3 객체이다. 
#> [1] "S3"
otype(c("A")) # vector는 S3 객체가 아니다. R 베이스타입이다. 
#> [1] "base"
otype(c(1,2,3)) 
#> [1] "base"
otype(array(1:3, 3)) # array는 R 베이스타입이다.
#> [1] "base"
otype(matrix(1:3, 3)) # matrix 는 R 베이스타입이다. 
#> [1] "base"
otype(list(a=c(1:3))) # list 는 R 베이스타입이다. 
#> [1] "base"
otype(factor(1)) # factor는 S3 객체이다. 
#> [1] "S3"

S3 클래스 정의하기

# 한 번에 클래스를 생성하고 할당하기
# structuer(base object, class = class_name)
# structure 함수는 해당 attribute 를 갖는 객체를 만드는 함수이다. 
foo <- structure(list(), class="foo") 
foo
#> list()
#> attr(,"class")
#> [1] "foo"

# 클래스를 생성하고 난 후 설정하기
foo <- list()
class(foo) <- "foo"

상속 (Inheritance)

inherits(foo, "foo") # foo 객체가 foo class 를 상속하는지 체
#> [1] TRUE
  • 예를들어, glm 은 lm 클래스의 하위 클래스이다.
model <- glm(mpg ~ cyl + hp, data=mtcars) 
class(model) # glm class 는 이와 같이 상속받은 lm 클래스를 class 에 포함하고 있다. 
#> [1] "glm" "lm"
inherits(model, "lm") 
#> [1] TRUE
  • lm 에는 없고, glm 에는 있는 generic function 찾아보기
methods(class="glm") # lm 에 추가하여 glm 에 추가로 정의된 generic function 이다. 
#>  [1] add1           anova          coerce         confint       
#>  [5] cooks.distance deviance       drop1          effects       
#>  [9] extractAIC     family         formula        influence     
#> [13] initialize     logLik         model.frame    nobs          
#> [17] predict        print          residuals      rstandard     
#> [21] rstudent       show           slotsFromS3    summary       
#> [25] vcov           weights       
#> see '?methods' for accessing help and source code
methods(class="lm") 
#>  [1] add1           alias          anova          case.names    
#>  [5] coerce         confint        cooks.distance deviance      
#>  [9] dfbeta         dfbetas        drop1          dummy.coef    
#> [13] effects        extractAIC     family         formula       
#> [17] hatvalues      influence      initialize     kappa         
#> [21] labels         logLik         model.frame    model.matrix  
#> [25] nobs           plot           predict        print         
#> [29] proj           qr             residuals      rstandard     
#> [33] rstudent       show           simulate       slotsFromS3   
#> [37] summary        variable.names vcov          
#> see '?methods' for accessing help and source code

클래스의 생성자 만들기

  • 생성자 함수는 일반적으로 클래스와 동일한 이름을 갖도록 한다.
foo <- function(x) { 
  if (!is.numeric(x)) stop("X must be numeric")
  structure(list(x), class="foo")
}
  • 생성자를 통해 변수 생성하기
foo_var <- foo(c(1,2,3)) 
print(foo_var)
#> [[1]]
#> [1] 1 2 3
#> 
#> attr(,"class")
#> [1] "foo"
str(foo_var) 
#> List of 1
#>  $ : num [1:3] 1 2 3
#>  - attr(*, "class")= chr "foo"
typeof(foo_var)
#> [1] "list"
otype(foo_var)
#> [1] "S3"
class(foo_var)
#> [1] "foo"
length(foo_var) # length generic function 의 default 가 실행되며, 여기서 base type 이 list 임을 확인하고, list 의 길이 1을 반환한다.
#> [1] 1
  • R 에서는 기존 객체의 클래스를 변경할 수 있다.
mod <- lm(log(mpg) ~ log(disp), data = mtcars) 
class(mod) 
#> [1] "lm"
typeof(mod)  
#> [1] "list"
print(mod)
#> 
#> Call:
#> lm(formula = log(mpg) ~ log(disp), data = mtcars)
#> 
#> Coefficients:
#> (Intercept)    log(disp)  
#>      5.3810      -0.4586
  • lm 클래스에 데이터프레임 class 를 추가하기
class(mod) <- "data.frame"
print(mod) # data frame s3 method 를 호출한다. 
#>  [1] coefficients  residuals     effects       rank          fitted.values
#>  [6] assign        qr            df.residual   xlevels       call         
#> [11] terms         model        
#> <0 rows> (or 0-length row.names)
mod$coefficients
#> (Intercept)   log(disp) 
#>   5.3809725  -0.4585683

S3 새로운 메소드와 제너릭 생성하기

  • 새로운 제너릭을 만들고 싶을 대, UseMethod() 를 call 하는 function 을 만든다.
  • UseMethod 는 제너릭 메소드의 이름과, argument를 input 으로 받는다.
f <- function(x) UseMethod("f") # 이렇게하면 f 라는 이름의 제너릭이 생성된 것이다. 

# 제너릭은 메소드가 없다면 쓸모가 없다. 아래와 같이 제너릭 메소드를 구현할 수 있다.
f.a <- function(x) "Class a" 
a <- structure(list(), class = "a") 
class(a) 
#> [1] "a"
  • 제너릭 함수의 호출
f(a)
#> [1] "Class a"
  • 원래 있는 제너릭에 메소드를 추가하기
mean.a <- function(x) "a"
mean(a)
#> [1] "a"

메소드 디스패치

  • S3에서 메소드 디스패치하는 법은 심플하다.
  • default 메소드를 정의하면 해당 클래스에 대한 메소드가 없을 경우 실행됨
f <- function(x) UseMethod("f") 
f.a <- function(x) "Class a" 
f.default <- function(x) "Unknown class" 

f(structure(list(), class = "a"))
#> [1] "Class a"
# b메소드에 대한 메소드가 없기 때문에 a 클래스에 대한 행된다. 
f(structure(list(), class = c("b", "a")))
#> [1] "Class a"
# c class에 대한 메소드 구현이 없기 때문에 default 메소드가 실행된다.
f(structure(list(), class=c("c"))) 
#> [1] "Unknown class"
c <- structure(list(), class = "c")
# Call the correct method:
f.default(c)
#> [1] "Unknown class"
  • 다른 클래스의 제네릭 메소드를 실행할 수도 있다.
f.a(c)
#> [1] "Class a"
  • S3 object가 아닌 것도, S3 제너릭 메소드를 실행할 수 있다.
  • 이 경우 R base type 을 이용해 메소드를 실행한다.
  • 이 R base type 을 알아내는 것은 힘들 수 있지만 아래와 같은 함수로 가능하다.
iclass <- function(x) {
  if (is.object(x)) {
    stop("x is not a primitive type", call. = FALSE)
  }

  c(
    if (is.matrix(x)) "matrix",
    if (is.array(x) && !is.matrix(x)) "array",
    if (is.double(x)) "double",
    if (is.integer(x)) "integer",
    mode(x)
  )
}
iclass(matrix(1:5)) 
#> [1] "matrix"  "integer" "numeric"
iclass(array(1.5))
#> [1] "array"   "double"  "numeric"

Group 제너릭

  • Group 제너릭이라는 것도 있는데 상당히 advanced 된 내용이다.
  • 여러개의 제너릭들을 한데 모아, 다양한 클래스의 제너릭 메소드를 정의함
  • 예를 들어, abs, sign, sqrt 등의 제너릭은 Math라는 이름의 그룹 제너릭이다.

Exercises

  • Read the source code for t() and t.test() and confirm that t.test() is an S3 generic and not an S3 method. What happens if you create an object with class test and call t() with it
# t 는 원래 matrix 나 datafame 등을 받아, transpose 를 return 하는 함수이다. 
array(1:6, list(2,3))
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6
t(array(1:6, list(2,3)))
#>      [,1] [,2]
#> [1,]    1    2
#> [2,]    3    4
#> [3,]    5    6
# t.test 는 제너릭이다.
a <- structure(c(1,2,3,4,5), class="test")

# 해당 코드는 one sample t-test 를 실행한다! 
# test 클래스를 보고, t.test 를 실행하기 때문 
# 이것은 t.test가 t 제너릭 메소드가 아니라 제너릭이기 때문이다. 
# 만약에 t.test가 t의 제너릭 메소드였다면, t(c(1,2,3,4,5)) 가 실행될 것이다. 
# 따라서 어떤 generic 을 생성할 때는 .을 포함하지 않도록 하는 것이 좋다. 
# t 를 실행하고 싶은데, t.test가 실행되는 등의 현상이 발생할 수 있다. 
t(a)
#> 
#>  One Sample t-test
#> 
#> data:  a
#> t = 4.2426, df = 4, p-value = 0.01324
#> alternative hypothesis: true mean is not equal to 0
#> 95 percent confidence interval:
#>  1.036757 4.963243
#> sample estimates:
#> mean of x 
#>         3
# 이런것도 가능하다. 
t.test <- function(a){
  print(a)
}
t(a)
#> [1] 1 2 3 4 5
#> attr(,"class")
#> [1] "test"
rm(t.test)
  • What classes have a method for the Math group generic in base R? Read the source code. How do the methods work?
  • abbc, sign, sqrt 등은 “Math” 그룹 제너릭이다.

Group “Math”:

abs, sign, sqrt, floor, ceiling, trunc, round, signif … 등등

methods(Math)
#> [1] Math,nonStructure-method Math,structure-method   
#> [3] Math.Date                Math.POSIXt             
#> [5] Math.data.frame          Math.difftime           
#> [7] Math.factor             
#> see '?methods' for accessing help and source code
  • R has two classes for representing date time data, POSIXct and POSIXlt, which both inherit from POSIXt. Which generics have different behaviours for the two classes? Which generics share the same behaviour?
methods(class="POSIXt") # 세 클래스에서 공통으로 정의된 제너릭 메소드
#>  [1] +            -            Axis         Math         Ops         
#>  [6] all.equal    as.character coerce       cut          diff        
#> [11] hist         initialize   is.numeric   julian       months      
#> [16] pretty       quantile     quarters     round        seq         
#> [21] show         slotsFromS3  str          trunc        weekdays    
#> see '?methods' for accessing help and source code
methods(class="POSIXct") # POSIXct 의 제너릭 메소드. 이 메소드는 POSIXt 에 추가하여 POSIXct 에서 새롭게 구현된 것이다.
#>  [1] Summary       [             [<-           [[            as.Date      
#>  [6] as.POSIXlt    as.data.frame as.list       c             coerce       
#> [11] format        initialize    length<-      mean          print        
#> [16] rep           show          slotsFromS3   split         summary      
#> [21] weighted.mean xtfrm        
#> see '?methods' for accessing help and source code
methods(class="POSIXlt") # POSIXlt 의 제너릭 메소드. 메 메소드는 마찬가지로 POSIXlt 에서 새롭게 구현된 것이다. 
#>  [1] Summary       [             [<-           [[            anyNA        
#>  [6] as.Date       as.POSIXct    as.data.frame as.double     as.list      
#> [11] as.matrix     c             coerce        duplicated    format       
#> [16] initialize    is.na         length        length<-      mean         
#> [21] names         names<-       print         rep           show         
#> [26] slotsFromS3   sort          summary       unique        weighted.mean
#> [31] xtfrm        
#> see '?methods' for accessing help and source code
  • Which base generic has the greatest number of defined methods?
library("methods") 
objs <- mget(ls("package:base"), inherits = TRUE)
funs <- Filter(is.function, objs) 
generics <- Filter(function(x) ("generic" %in% pryr::ftype(x)), funs)
  
sort(
  lengths(sapply(names(generics), function(x) methods(x), USE.NAMES = TRUE)),
  decreasing = TRUE
  )[1]
#> print 
#>   208
  • UseMethod() calls methods in a special way. Predict what the following code will return, then run it and read the help for UseMethod() to figure out what’s going on. Write down the rules in the simplest form possible.
y <- 1
g <- function(x) { 
  y <- 2
  UseMethod("g")
}
g.numeric <- function(x) y
g(10) # Internal variable 을 먼저 찾기 때문에 2가 된다.
#> [1] 2
#> [1] 2

h <- function(x) {
  x <- 10
  UseMethod("h")
}
h.character <- function(x) paste("char", x)
h.numeric <- function(x) paste("num", x)

h("a")
#> [1] "char a"
#> [1] "char a"
  • Internal generics don’t dispatch on the implicit class of base types. Carefully read ?“internal generic” to determine why the length of f and g is different in the example below. What function helps distinguish between the behaviour of f and g?
f <- function() 1 
g <- function() 2 
class(g) <- "function" 
class(f) 
#> [1] "function"
#> [1] "function"
class(g)
#> [1] "function"
#> [1] "function"

length.function <- function(x) "function" 

length(f)
#> [1] 1
#> [1] 1
length(g)
#> [1] "function"
#> [1] "function"

Answer : f는 implicit class 가 function 인 것이고, g 는 class=function 으로 정의된 것이다.

library(pryr)
# fytype 함수는 해당 function 이 internal/S3/S4/RC 인지를 알려준다. 
ftype(f) 
#> [1] "function"
ftype(g) 
#> [1] "function"
ftype(t.test) 
#> [1] "s3"      "generic"
ftype(length) 
#> [1] "primitive" "generic"
# 둘 다 function 으로 되는데 이것이 implicit class 인지는 아래처럼 확인할 수 있다. 
is.object(f) # f는 implicit class "function"" 이기 때문에 FALSE가 반환된다. 
#> [1] FALSE
is.object(g) # g는 s3 객체이기 때문에 TRUE 가 반환된다.
#> [1] TRUE


반응형
반응형

R 트렌드를 볼 수 있는 블로그


해들리 위컴은 R 과 data science 의 최신 기법을 항상 숙지하고 있기 위해 300개 이상의 블로그를 follow-up 한다고 한다. 그만큼 최신 정보를 빠르게 습득하는 것은 매우 중요하다고 할 수 있다. 본 포스팅에서는 R 관련 유명 블로그들 중 몇 개를 추려 포스팅해보고자 한다. 


1. R studio blog  (https://blog.rstudio.com/, Since 2011)


R Studio 가 R 에서 차지하는 위상이 높은 만큼, R studio 의 블로그도 R 커뮤니티에서 매우 유명한 블로그이다. R studio 의 자체적인 라이브러리들과 상업적인 프로그램들에 관한 뉴스와 여러 이벤트들에 관한 글도 올라온다. 한 달에 4개 주기로 글이 올라온다. 


2. R Bloggers (https://www.r-bloggers.com/, Since 2005)



R-bloggers는 가장 유명한 R 블로그라고 할 수 있다. 가장 오래된 블로그이기도 하다. (Since 2005). R-bloggers 는 R 을 통해 효과적이고 효율적인 어플리케이션, 솔루션을 만들기 위한 튜토리얼과 팁들에 관하여 집중적으로 글이 올라온다. 한달에 34개 주기의 글이 올라온다. 


3. Simply statistics (https://simplystatistics.org/, Since 2011)


Simply statistics 블로그는 R 관련 MOOC 의 강의자들이 협력하여 만든 블로그이다. R과 Data science 관련한 전반적인 글이 올라온다. 기술적인 글 보다는 Interview, News 등 가벼운 주제 에 관한 글이 주로 올라오는듯해 가볍게 볼 수 있는 블로그이다. 글이 많이 올라오지 않으나 깊이가 있다. 


아래는 이 블로그의 주인들과 관련된 MOOC 목록이다. 


Data Science Specialization on Coursera

by Jeff Leek, Roger Peng, Brian Caffo. 


Data analysis for life sciences on edx

Rafael Irizarry and Mike Love
health science 와 genomics 에 초점을 맞춘 강의이다. 


Genomic Data Science Specialization on Coursera

Jeff Leek, Steven Salzberg, James Taylor, Ela Pertea, Liliana Florea, Ben Langmead, and Kasper Hansen

genome data 에 초점을 맞춘 강의이다.


4. Yihui's Blog | R statistics (yihui.name/en, Since 2007)



Yihui Xie 는 R studio 의 software engineer 이다. knitr, bookdown, blogdown, xaringan, DT 등의 유명한 R 패키지를 만든 R 커뮤니티에서 이름값이 높은 사람이다. 또한 그는 2008년부터 중국의 R 컨퍼런스를 만들고 운영하고 있다. 그가 R 에 관해 갖고 있는 경험과 생각을 배우고 싶다면 follow 하면 좋다. (1주에 2개의 포스트 정도가 올라온다.)


5. Shirin's playgRound (shirinsplayground.netlify.com, Since 2017)


Shirin 은 원래 생물학자였으나, 생명정보학자 그리고 데이터 과학자로 직업을 바꾸며 활동하고 있다. 그녀는 머신러닝과 데이터 시각화에 관심이 많고, 항상 모든 일을 R 을 사용해 처리한다. 이 블로그를 follow 하면, 그녀가 일을 할 때 어떤 Tool 을 선택하고, R 에 관해 어떤 생각을 갖고 있는지를 알 수 있다. (1달에 4개 정도의 포스트가 올라온다.) 


6. R views https://rviews.rstudio.com/

-An R community blog edited by RStudio


R studio 가 관리하는 블로그이다. R 이용 관련 상당히 유용한 정보들이 많이 이용하는 듯 하다.


* 이외에도 수많은 R 블로그들이 있다. 이 포스트는 R 의 유명한 블로그들 40개를 정리하였다.  

* Feed spot (https://www.feedspot.com) 은 이러한 블로그들을 follow 할 수 있는 플랫폼이다. 무료 유저의 경우 12개의 사이트를 follow 할 수 있으며, 새로운 글이 올라왔을 때, 이를 간편하게 확인할 수 있다. 

반응형
반응형

R Studio 팁과 트릭


1. Alt-Shift-K


R studio 에서 쓸 수 있는 shortcut 을 보여준다. shortcut 을 바꾸고 싶으면 tools-modify keyboard shortcut 을 선택한다.


2. ctrl+shift+f


ctrl+f 는 현재 파일에서 해당문자를 찾는다면, ctrl+shift+f 는 현재 프로젝트에서 해당문자를 찾는다. 이것저것 파일을 뒤져가며 찾지말고 ctrl+shift+f 를 활용하자. 


3. code snippet 


Tools-Global options-Code-Edit code snippet


mat 를 입력한 후, shift-tab 을 누르면, matrix 생성자와 인자가 자동완성된다. 이를 code snippet 이라 하는데, 이는 함수의 구조를 자동완성으로 쉽게 타이핑할 수 있으며, 자주 사용하는 코드를 복사-붙여넣기가 아니라 단축키를 통해 만들어낼 수 있다는 장점이 있다.


4. Navigation


tab 을 navigation 하기 위해 마우스보다는 키보드 단축키를 활용하자. 


현재 tab을 닫을 때는 ctrl+w

모든 tab을 닫을 때는 ctrl+shift+w

현재 tab을 제외하고 모두 닫을 때는 ctrl+alt+sift+w (네 손가락을 이용하기 때문에 힘들 수 있다.)


그리고 navigation 할 때는 ctrl+. 을 활용하면 좋다. 



참고

https://www.youtube.com/watch?v=kuSQgswZdr8&list=PLL8uQat--_mDkJfhpsYDhPyNvCEsNy0er&index=3

반응형
반응형