ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ML-2] 파이썬 _ 판다스(Pandas) 패키지
    머신러닝 2022. 1. 11. 23:24
    • 판다스(Pandas) : 행과 열로 구성된 2차원 데이터를 효율적으로 가공 및 처리할 수 있는 패키지
    • 데이터프레임(DataFrame) : 판다스(Pandas)의 핵심 객체 / 여러 개의 행과 열로 이뤄진 2차원 데이터를 담는 데이터 구조체 / 칼럼(column)이 여러 개인 데이터 구조체
    import pandas as pd # pandas를 pd로 alias해 임포트하는 것이 관례

    여기서 데이터셋은 캐글(Kaggle)에서 제공하는 타이타닉 탑승자 파일을 사용한다.

    캐글에서 다운 받은 'titanic_train.csv' 파일을 사용하는데 여기서 확장자 '.csv'는 칼럼(Column)들이 ','로 구분되어 있는 파일 포맷을 얘기한다. 다운 받은 데이터를 보면 아래와 같이 ','로 구분되어 있는 있는 것을 확인할 수 있다.

    해당 데이터를 판다스 API를 이용하여 불러오게 되면 데이터셋은 데이터프레임 객체로 존재하게 된다.

    titanic_df=pd.read_csv('titanic_train.csv')#read_csv() API는 csv포맷의 파일을 DataFrame 객체로 가져오게 함 
    print(titanic_df)
    [결과] 
         PassengerId  Survived  Pclass  ...     Fare Cabin  Embarked
    0              1         0       3  ...   7.2500   NaN         S
    1              2         1       1  ...  71.2833   C85         C
    2              3         1       3  ...   7.9250   NaN         S
    3              4         1       1  ...  53.1000  C123         S
    4              5         0       3  ...   8.0500   NaN         S
    ..           ...       ...     ...  ...      ...   ...       ...

    .csv의 데이터셋이 DataFrame으로 변경된 것을 확인할 수 있다.

    각각의 칼럼을 제외하고 칼럼명이 명시되어 있지 않은 데이터는 Index다. 모든 DataFrame 내의 데이터는 생성되는 순간 고유의 Index 값을 가지게 된다.

    print('titanic_df의 크기 : ', titanic_df.shape) # shape 변수는 DataFrame의 행과 열을 튜플 형태로 반환함
    [결과]
    titanic_df의 크기 :  (891, 12) # 891개의 row와 12개의 col로 구성
    value_counts=titanic_df['Pclass'].value_counts() 
    """
    DataFrame의 []내부에 칼럼명을 입력하면 Series 형태로 특정 칼럼 데이터셋이 반환됨
    value_counts() 메서드는 데이터의 분포를 확인하는 함수
    호출 시 해당 칼럼값의 유형과 건수를 확인할 수 있음
    """
    print(value_counts)
    [결과]
    3    491
    1    216
    2    184
    """
    칼럼 'Pclass'는 3,1,2를 가지고 
    3이 491개, 1이 216개, 2가 184개 있음
    value_counts() 메서드는 많은 건수 순서로 정렬되어 반환함
    """

     

    [ndarray,리스트,딕셔너리를 DataFrame으로 변환]

     

    DataFrame은 리스트,ndarray와는 달리 칼럼명을 가지고 있다. 따라서 칼럼명을 지정해서 변환해주어야 한다(지정하지 않으면 자동으로 칼럼명을 할당함). 

    """
    1차원 리스트, ndarry
    """
    col_name1=['col1'] # 칼럼명 지정
    list1=[1,2,3] # 리스트
    array1=np.array(list1) # ndarray
    df_list1=pd.DataFrame(list1,columns=col_name1) # 리스트 -> DataFrame
    print('리스트로 만든 DataFrame:\n',df_list1)
    df_array1=pd.DataFrame(array1,columns=col_name1) # ndarray -> DataFrame
    print('ndarray로 만든 DataFrame:\n',df_array1)
    [결과]
    리스트로 만든 DataFrame:
        col1
    0     1
    1     2
    2     3
    ndarray로 만든 DataFrame:
        col1
    0     1
    1     2
    2     3
    """
    2차원 리스트, ndarray
    DataFrame은 2차원 데이터이므로 2차원 이하의 데이터들만 변환 가능
    """
    col_name2=['col1','col2','col3'] # 칼럼명 지정
    list2=[[1,2,3],
           [11,12,13]] # 2차원 리스트
    array2=np.array(list2) # 2차원 ndarray
    df_list2=pd.DataFrame(list2,columns=col_name2) # 2차원 리스트 -> DataFrame
    print('2차원 리스트로 만든 DataFrame:\n',df_list2)
    df_array2=pd.DataFrame(array2,columns=col_name2) # 2차원 ndarray -> DataFrame
    print('2차원 ndarray로 만든 DataFrame:\n',df_array2)
    [결과]
    2차원 리스트로 만든 DataFrame:
        col1  col2  col3
    0     1     2     3
    1    11    12    13
    2차원 ndarray로 만든 DataFrame:
        col1  col2  col3
    0     1     2     3
    1    11    12    13
    """
    딕셔너리를 DataFrame으로 변환
    딕셔너리의 Key -> Col
    딕셔너리의 Value -> Data
    """
    dict={'col1':[1,11],'col2':[2,22],'col3':[3,33]}
    df_dict=pd.DataFrame(dict) # 딕셔너리 -> DataFrame
    print('딕셔너리로 만든 DataFrame:\n',df_dict)
    [결과]
    딕셔너리로 만든 DataFrame:
        col1  col2  col3
    0     1     2     3
    1    11    22    33

    [DataFrame을 ndarray,리스트,딕셔너리로 변환]

     

    머신러닝 패키지에서 기본 데이터 형으로 ndarray를 사용한다. 따라서 DataFrame을 ndarray로 변환하는 경우가 빈번하게 발생한다.

    array3=df_dict.values # values를 이용하면 간단하게 ndarray로 변환
    print('DataFrame을 ndarray로 변환:\n',array3)
    [결과]
     [[ 1  2  3]
     [11 22 33]]
     """
     리스트로의 변환은 values로 얻은 ndarray에 .tolist() 호출
     딕셔너리로의 변환은 DataFrame 객체의 to_dict() 메서드 호출, 인자로 'list'입력 시 
     딕셔너리의 값은 리스트형으로 반환 
     """
    list3=df_dict.values.tolist() # list3=array3.tolist()
    print('DataFrame을 리스트로 변환:\n',list3)
    dict3=df_dict.to_dict('list')
    print('DataFrame을 딕셔너리로 변환:\n',dict3)
    [결과]
    DataFrame을 리스트로 변환:
     [[1, 2, 3], [11, 22, 33]]
    DataFrame을 딕셔너리로 변환:
     {'col1': [1, 11], 'col2': [2, 22], 'col3': [3, 33]}

    [DataFrame 데이터 관리]

    """
    칼럼 데이터 생성 및 수정
    """
    titanic_df['new col']=0 # 새로운 칼럼 'new col'을 추가하고 일괄적으로 0 값 할당
    print(titanic_df)
    [결과]
         PassengerId  Survived  Pclass  ... Cabin Embarked  new col
    0              1         0       3  ...   NaN        S        0
    1              2         1       1  ...   C85        C        0
    2              3         1       3  ...   NaN        S        0
    3              4         1       1  ...  C123        S        0
    4              5         0       3  ...   NaN        S        0
    ..           ...       ...     ...  ...   ...      ...      ...
    """
    새로운 Series가 기존 DataFrame에 추가되는 것
    따라서 기존 칼럼의 Series의 데이터를 이용하여 새로운 칼럼도 만들 수 있음
    """
    titanic_df['new col2']=titanic_df['new col']+100
    print(titanic_df)
    [결과]
         PassengerId  Survived  Pclass  ... Embarked new col  new col2
    0              1         0       3  ...        S       0       100
    1              2         1       1  ...        C       0       100
    2              3         1       3  ...        S       0       100
    3              4         1       1  ...        S       0       100
    4              5         0       3  ...        S       0       100
    ..           ...       ...     ...  ...      ...     ...       ...
    """
    수정 또한 같은 방법으로 할 수 있음
    """
    titanic_df['new col2']+=100
    print(titanic_df)
    [결과]
         PassengerId  Survived  Pclass  ... Embarked new col  new col2
    0              1         0       3  ...        S       0       200
    1              2         1       1  ...        C       0       200
    2              3         1       3  ...        S       0       200
    3              4         1       1  ...        S       0       200
    4              5         0       3  ...        S       0       200
    ..           ...       ...     ...  ...      ...     ...       ...
    """
    DataFrame 데이터 삭제
    
    drop() 메서드 활용
    drop() 메서드의 원형 
    DataFrame.drop(labels=None, axis=0, index=None, Columns=None, level=None, inplace=False, errors='raise')
    axis의 값에 따라서 칼럼 또는 행을 삭제함(axis=0 -> row , axis=1 -> col)
    label에는 원하는 칼럼명 입력
    """
    titanic_df_drop=titanic_df.drop('new col2',axis=1) # 'new col2' 칼럼을 드롭
    print(titanic_df_drop)
    [결과]
         PassengerId  Survived  Pclass  ... Cabin Embarked  new col
    0              1         0       3  ...   NaN        S        0
    1              2         1       1  ...   C85        C        0
    2              3         1       3  ...   NaN        S        0
    3              4         1       1  ...  C123        S        0
    4              5         0       3  ...   NaN        S        0
    ..           ...       ...     ...  ...   ...      ...      ...
    """
    하지만 'new col2' 칼럼을 드롭한 후에 원본 데이터셋을 보면
    여전히 'new col2' 칼럼이 존재하는 것을 볼 수 있음
    """
    print(titanic_df)
    [결과]
         PassengerId  Survived  Pclass  ... Embarked new col  new col2
    0              1         0       3  ...        S       0       200
    1              2         1       1  ...        C       0       200
    2              3         1       3  ...        S       0       200
    3              4         1       1  ...        S       0       200
    4              5         0       3  ...        S       0       200
    """
    원본 데이터셋 자체를 삭제하고 싶다면 
    inplece=True 로 설정해주어야 함(inplace의 디폴트 값은 False)
    """
    titanic_df.drop('new col2',axis=1,inplace=True) # inplace=True 입력 시 반환x
    print(titanic_df)
    [결과] # 원본 데이터에서도 삭제됨을 확인할 수 있음 
         PassengerId  Survived  Pclass  ... Cabin Embarked  new col
    0              1         0       3  ...   NaN        S        0
    1              2         1       1  ...   C85        C        0
    2              3         1       3  ...   NaN        S        0
    3              4         1       1  ...  C123        S        0
    4              5         0       3  ...   NaN        S        0
    ..           ...       ...     ...  ...   ...      ...      ...

    [데이터 셀렉션 및 필터링]

     

    DataFrame 뒤의 '[]'는 칼럼만 지정할 수 있는 '칼럼 지정 연산자'로 이해하는 것이 좋다.

    ix[] 연산자는 코드에 혼돈을 주거나 가독성을 떨어지게 하기 때문에 대신 loc[] 연산자와 iloc[] 연산자를 사용한다.

    loc[] 연산자와 iloc[] 연산자를 살펴보기 이전에 

    '명칭 기반 인덱싱'과 '위치 기반 익덱싱'을 구분해야 한다.

    '명칭(Label) 기반 인덱싱'은 칼럼의 명칭을 기반으로 위치를 지정하는 방식을 말한다. 

    '위치(Position) 기반 인덱싱'은 0을 출발점으로 하는 가로축, 세로축 좌표 기반의 행/열 위치를 기반으로 지정하는 방식을 말한다.

    DataFrame의 익덱스 값은 '명칭(Label) 기반 인덱싱'으로 간주해야 한다.

     

    1. iloc 연산자 : 위치 기반 인덱싱, int형, int형 슬라이싱, 팬시 리스트 값 입력

    """
         PassengerId  Survived  Pclass  ...     Fare Cabin  Embarked
    0              1         0       3  ...   7.2500   NaN         S
    1              2         1       1  ...  71.2833   C85         C
    2              3         1       3  ...   7.9250   NaN         S
    3              4         1       1  ...  53.1000  C123         S
    4              5         0       3  ...   8.0500   NaN         S
    ..           ...       ...     ...  ...      ...   ...       ...
    886          887         0       2  ...  13.0000   NaN         S
    887          888         1       1  ...  30.0000   B42         S
    888          889         0       3  ...  23.4500   NaN         S
    889          890         1       1  ...  30.0000  C148         C
    890          891         0       3  ...   7.7500   NaN         Q
    """
    print(titanic_df.iloc[0,2]) # 첫번째 행의 3번째 열 값 출력
    print('----')
    print(titanic_df.iloc[:5,2]) # 첫번째 행 ~ 5번째 행의 3번째 열 값 출력 
    [결과]
    3
    ----
    0    3
    1    1
    2    3
    3    1
    4    3

    2. loc[] 연산자 : 명칭 기반 인덱싱, loc[index 값,칼럼 명]

    """
         PassengerId  Survived  Pclass  ...     Fare Cabin  Embarked
    0              1         0       3  ...   7.2500   NaN         S
    1              2         1       1  ...  71.2833   C85         C
    2              3         1       3  ...   7.9250   NaN         S
    3              4         1       1  ...  53.1000  C123         S
    4              5         0       3  ...   8.0500   NaN         S
    ..           ...       ...     ...  ...      ...   ...       ...
    886          887         0       2  ...  13.0000   NaN         S
    887          888         1       1  ...  30.0000   B42         S
    888          889         0       3  ...  23.4500   NaN         S
    889          890         1       1  ...  30.0000  C148         C
    890          891         0       3  ...   7.7500   NaN         Q
    """
    print(titanic_df.loc[0,'Embarked'])
    print('----')
    print(titanic_df.loc[1:5,'Cabin'])
    [결과]
    S
    ----
    1     C85
    2     NaN
    3    C123
    4     NaN
    5     NaN
    """
    loc[] 연산자는 '명칭 기반 인덱싱' 이므로 
    슬라이싱 범위를 [1:5]로 입력했을 때 
    1,2,3,4,5 index 가 출력되는 것을 인지해야 함.
    '위치 기반 인덱싱' iloc[] 연산자였다면 1,2,3,4 index가 출력
    이 둘의 차이를 이해할 필요가 있음
    
    "명칭 기반 인덱싱"은 슬라이싱을 '시작점:종료점'으로 지정할 때 시작점에서 종료점을 포함한 위치에 있는 
    데이터를 반환하는구나"
    """

    3. 불린 인덱싱

    """
         PassengerId  Survived  Pclass  ...     Fare Cabin  Embarked
    0              1         0       3  ...   7.2500   NaN         S
    1              2         1       1  ...  71.2833   C85         C
    2              3         1       3  ...   7.9250   NaN         S
    3              4         1       1  ...  53.1000  C123         S
    4              5         0       3  ...   8.0500   NaN         S
    ..           ...       ...     ...  ...      ...   ...       ...
    886          887         0       2  ...  13.0000   NaN         S
    887          888         1       1  ...  30.0000   B42         S
    888          889         0       3  ...  23.4500   NaN         S
    889          890         1       1  ...  30.0000  C148         C
    890          891         0       3  ...   7.7500   NaN         Q
    """
    print(titanic_df[titanic_df['Age']>60]) # 'Age' 칼럼 값이 60 보다 큰 데이터를 모두 반환
    [결과]
         PassengerId  Survived  Pclass  ...      Fare        Cabin  Embarked
    33            34         0       2  ...   10.5000          NaN         S
    54            55         0       1  ...   61.9792          B30         C
    96            97         0       1  ...   34.6542           A5         C
    116          117         0       3  ...    7.7500          NaN         Q
    170          171         0       1  ...   33.5000          B19         S
    252          253         0       1  ...   26.5500          C87         S
    275          276         1       1  ...   77.9583           D7         S
    280          281         0       3  ...    7.7500          NaN         Q
    326          327         0       3  ...    6.2375          NaN         S
    438          439         0       1  ...  263.0000  C23 C25 C27         S
    456          457         0       1  ...   26.5500          E38         S
    483          484         1       3  ...    9.5875          NaN         S
    493          494         0       1  ...   49.5042          NaN         C
    545          546         0       1  ...   26.0000          NaN         S
    555          556         0       1  ...   26.5500          NaN         S
    570          571         1       2  ...   10.5000          NaN         S
    625          626         0       1  ...   32.3208          D50         S
    630          631         1       1  ...   30.0000          A23         S
    672          673         0       2  ...   10.5000          NaN         S
    745          746         0       1  ...   71.0000          B22         S
    829          830         1       1  ...   80.0000          B28       NaN
    851          852         0       3  ...    7.7750          NaN         S
    
    print(titanic_df.loc[titanic_df['Age']>60,['Name','Age']])
    #'Age' 칼럼 값이 60보다 큰 데이터 중에서 'Name','Age' 칼럼만 반환
    [결과]
                                              Name   Age
    33                       Wheadon, Mr. Edward H  66.0
    54              Ostby, Mr. Engelhart Cornelius  65.0
    96                   Goldschmidt, Mr. George B  71.0
    116                       Connors, Mr. Patrick  70.5
    170                  Van der hoef, Mr. Wyckoff  61.0
    252                  Stead, Mr. William Thomas  62.0
    275          Andrews, Miss. Kornelia Theodosia  63.0
    280                           Duane, Mr. Frank  65.0
    326                  Nysveen, Mr. Johan Hansen  61.0
    438                          Fortune, Mr. Mark  64.0
    456                  Millet, Mr. Francis Davis  65.0
    483                     Turkula, Mrs. (Hedwig)  63.0
    493                    Artagaveytia, Mr. Ramon  71.0
    545               Nicholson, Mr. Arthur Ernest  64.0
    555                         Wright, Mr. George  62.0
    570                         Harris, Mr. George  62.0
    625                      Sutton, Mr. Frederick  61.0
    630       Barkworth, Mr. Algernon Henry Wilson  80.0
    672                Mitchell, Mr. Henry Michael  70.0
    745               Crosby, Capt. Edward Gifford  70.0
    829  Stone, Mrs. George Nelson (Martha Evelyn)  62.0
    851                        Svensson, Mr. Johan  74.0
    
    print(titanic_df[(titanic_df['Age']>60)&(titanic_df['Pclass']==1)
          &(titanic_df['Sex']=='female')])
    # 60세 이상의 선실 등급 1등극인 승객 중 여성 승객만 추출(복합 조건 연산자 사용)
    [결과]
         PassengerId  Survived  Pclass  ...     Fare Cabin  Embarked
    275          276         1       1  ...  77.9583    D7         S
    829          830         1       1  ...  80.0000   B28       NaN

    [정렬, Aggregation, GroupBy]

    1. 정렬 : sort_values()

    주요 입력 파라미터로는 by, ascending, inplace가 있다. 

    by는 특정 칼럼을 입력하면 해당 칼럼으로 정렬을 수행하며, ascending을 디폴트 값이 True(오름차순)이다. 

    inplace는 앞서 살펴본 바와 같이 디폴트 값이 False이다.

    """
         PassengerId  Survived  Pclass  ...     Fare Cabin  Embarked
    0              1         0       3  ...   7.2500   NaN         S
    1              2         1       1  ...  71.2833   C85         C
    2              3         1       3  ...   7.9250   NaN         S
    3              4         1       1  ...  53.1000  C123         S
    4              5         0       3  ...   8.0500   NaN         S
    ..           ...       ...     ...  ...      ...   ...       ...
    886          887         0       2  ...  13.0000   NaN         S
    887          888         1       1  ...  30.0000   B42         S
    888          889         0       3  ...  23.4500   NaN         S
    889          890         1       1  ...  30.0000  C148         C
    890          891         0       3  ...   7.7500   NaN         Q
    """
    sort_by_age=titanic_df.sort_values(by=['Age']) # 'Age' 칼럼 기준 오름차순 정렬 
    print(sort_by_age)
    [결과]
         PassengerId  Survived  Pclass  ...     Fare Cabin  Embarked
    803          804         1       3  ...   8.5167   NaN         C
    755          756         1       2  ...  14.5000   NaN         S
    644          645         1       3  ...  19.2583   NaN         C
    469          470         1       3  ...  19.2583   NaN         C
    78            79         1       2  ...  29.0000   NaN         S
    ..           ...       ...     ...  ...      ...   ...       ...
    859          860         0       3  ...   7.2292   NaN         C
    863          864         0       3  ...  69.5500   NaN         S
    868          869         0       3  ...   9.5000   NaN         S
    878          879         0       3  ...   7.8958   NaN         S
    888          889         0       3  ...  23.4500   NaN         S

    2. aggregation 함수 적용 _ min(), max(), sum(), count()

    DataFrame의 경우 DataFrame에 바로 agregation 호출할 경우 모든 칼럼에 적용된다. 따라서 대상 칼럼들만 추출하여 aggregation을 적용해주어야 한다.

    print(titanic_df[['Age']].min()) # 'Age' 칼럼의 최소값
    [결과]
    Age    0.42

    3. groupby() 적용

    DataFrame에 groupby()를 호출하면 DataFrameGroupBy라는 또 다른 형태의 DataFrame을 반환한다.

    """
    groupby()를 호출해 반환된 결과에 aggregation 함수를 호출할 수 있다
    """
    titanic_groupby=titanic_df.groupby('Pclass').count()
    print(titanic_groupby)
    [결과]
            PassengerId  Survived  Name  Sex  ...  Ticket  Fare  Cabin  Embarked
    Pclass                                    ...                               
    1               216       216   216  216  ...     216   216    176       214
    2               184       184   184  184  ...     184   184     16       184
    3               491       491   491  491  ...     491   491     12       491
    # -------------------------------
    titanic_groupby=titanic_df.groupby('Pclass')[['PassengerId','Survived']].count()
    # 이처럼 DataFrameGroupBy 객체에 [['PassengerId','Survived']]로 특정 칼럼을 필터링하여 aggregation 함수를 적용할 수 있음
    print(titanic_groupby)
    [결과]
            PassengerId  Survived
    Pclass                       
    1               216       216
    2               184       184
    3               491       491
    # -------------------------------
    agg_format={'Age':'max','SibSp':'sum','Fare':'mean'} # 딕셔너리 형태로 agg의 입력 값을 구성
    titanic_groupby=titanic_df.groupby('Pclass').agg(agg_format)
    # 'Pclass'칼럼에 대해, 'Age'칼럼의 최대 값, 'SibSp'칼럼의 총합, 'Fare'칼럼의 평균을 가지는 데이터셋
    print(titanic_groupby)
    [결과]
             Age  SibSp       Fare
    Pclass                        
    1       80.0     90  84.154687
    2       70.0     74  20.662183
    3       74.0    302  13.675550

    4. lambda 

    lambda 식은 파이썬에서 함수형 프로그래밍을 지원하기 위해 만들어졌다.

    아래와 같은 입력값의 제곱 값을 반환하는 get_square(a)라는 함수가 있다고 가정하자.

    def get_square(a):
    	return a**2

    이러한 함수를 lambda 식으로 변환하면 아래와 같다.

    lambda_square=lambda x : x**2
    # 입력인자 x에 대해서 x**2 값을 반환

    DataFrame에 활용해보면 

    titanic_df['Name_len']=titanic_df['Name'].apply(lambda x:len(x))
    # 'Name'칼럼의 값을 인자로 받아 길이를 'Name_len'칼럼에 저장
    print(titanic_df[['Name','Name_len']])
    [결과]
                                                      Name  Name_len
    0                              Braund, Mr. Owen Harris        23
    1    Cumings, Mrs. John Bradley (Florence Briggs Th...        51
    2                               Heikkinen, Miss. Laina        22
    3         Futrelle, Mrs. Jacques Heath (Lily May Peel)        44
    4                             Allen, Mr. William Henry        24
    ..                                                 ...       ...
    886                              Montvila, Rev. Juozas        21
    887                       Graham, Miss. Margaret Edith        28
    888           Johnston, Miss. Catherine Helen "Carrie"        40
    889                              Behr, Mr. Karl Howell        21
    890                                Dooley, Mr. Patrick        19

    조건문도 활용할 수 있다.

    titanic_df['Child_Adult']=titanic_df['Age'].apply(lambda x:'Child' if x<=15
                                                      else 'Adult')
    # 'Age'칼럼 값을 x로 받아 15보다 작거나 같으면 'Child' 아니면 'Adult' 반환하여 'Child_Adult'칼럼에 저장

    여기서 주의해야할 점은 lambda 식 ':' 기호의 오른편에 반환 값이 존재해야 한다는 것이다. 

    lambda x:if x<=15 'Child' else 'Adult'

    위와 같이 작성하면 안된다는 것이다. 

    '머신러닝' 카테고리의 다른 글

    [ML-1] 파이썬 _ 넘파이(Numpy) 패키지  (0) 2022.01.10
    [ML-0] 머신러닝이란  (0) 2022.01.10

    댓글

Designed by Tistory.