ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 데이터 정제 및 준비
    Python 2023. 11. 21. 12:21
    누락된 데이터 처리

     

    float64 dtype을 가지는 데이터의 경우 판다스는 실숫값인 NaN으로 누락된 데이터 표시

    이런 값을 감싯값(sentinel value)이라 부르며 누락된(혹은 null) 값임을 나타내기 위해 등장

    In [24]: float_data = pd.Series([1.2, -3.5, np.nan, 0])
    
    In [25]: float_data
    Out[25]:
    0    1.2
    1   -3.5
    2    NaN
    3    0.0
    dtype: float64
    
    # isna 메서드는 값이 null인 경우 True를 가지는 불리언 Series 반환
    In [26]: float_data.isna()
    Out[26]:
    0    False
    1    False
    2     True
    3    False
    dtype: bool

     

    파이썬의 내장 None 값 또한 NA 값으로 취급됨

    In [27]: string_data = pd.Series(['aardvark', np.nan, None, 'avocado'])
    
    In [28]: string_data
    Out[28]:
    0    aardvark
    1         NaN
    2        None
    3     avocado
    dtype: object
    
    In [29]: string_data.isna()
    Out[29]:
    0    False
    1     True
    2     True
    3    False
    dtype: bool
    
    In [30]: float_data = pd.Series([1, 2, None], dtype='float64')
    
    In [31]: float_data
    Out[31]:
    0    1.0
    1    2.0
    2    NaN
    dtype: float64
    
    In [32]: float_data.isna()
    Out[32]:
    0    False
    1    False
    2     True
    dtype: bool

     

    누락된 데이터 골라내기

    # Series에 dropna 메서드를 적용하면 null이 아닌 데이터와 인덱스 값만 들어 있는 Series 반환
    In [34]: data = pd.Series([1, np.nan, 3.5, np.nan,7])
    
    In [35]: data.dropna()
    Out[35]:
    0    1.0
    2    3.5
    4    7.0
    dtype: float64
    
    # 위와 동일한 코드
    In [36]: data[data.notna()]
    Out[36]:
    0    1.0
    2    3.5
    4    7.0
    dtype: float64

     

    DataFrame 객체의 경우 모두 NA인 행이나 열을 제외하거나 NA 값을 하나라도 포함하고 있는 행이나 열을 제외할 수도 있음

    dropna는 기본적으로 NA값이 하나라도 있는 행을 제외

    In [37]: data = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
        ...:                      [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
    
    In [38]: data
    Out[38]:
         0    1    2
    0  1.0  6.5  3.0
    1  1.0  NaN  NaN
    2  NaN  NaN  NaN
    3  NaN  6.5  3.0
    
    In [39]: data.dropna()
    Out[39]:
         0    1    2
    0  1.0  6.5  3.0
    
    # how='all' 옵션을 넘기면 모든 값이 NA인 행만 제외됨
    In [40]: data.dropna(how='all')
    Out[40]:
         0    1    2
    0  1.0  6.5  3.0
    1  1.0  NaN  NaN
    3  NaN  6.5  3.0

     

    이런 함수는 기본적으로 원본 객체 내용을 변경하지 않고, 새로운 객체 반환

    # 열을 제외하는 방법도 동일하게 작동, axis='columns'
    In [42]: data[4] = np.nan
    
    In [43]: data
    Out[43]:
         0    1    2   4
    0  1.0  6.5  3.0 NaN
    1  1.0  NaN  NaN NaN
    2  NaN  NaN  NaN NaN
    3  NaN  6.5  3.0 NaN
    
    In [44]: data.dropna(axis='columns', how='all')
    Out[44]:
         0    1    2
    0  1.0  6.5  3.0
    1  1.0  NaN  NaN
    2  NaN  NaN  NaN
    3  NaN  6.5  3.0
    
    In [45]: data.dropna(how='all')
    Out[45]:
         0    1    2   4
    0  1.0  6.5  3.0 NaN
    1  1.0  NaN  NaN NaN
    3  NaN  6.5  3.0 NaN
    
    In [46]: data.dropna(axis='index', how='all').dropna(axis='columns', how='all')
    Out[46]:
         0    1    2
    0  1.0  6.5  3.0
    1  1.0  NaN  NaN
    3  NaN  6.5  3.0

     

     

    # 결측치가 특정 개수보다 적은 행만 살펴보고 싶은 경우 thresh 인수에 원하는 값 설정
    In [47]: df = pd.DataFrame(np.random.standard_normal((7, 3)))
    
    In [48]: df.iloc[:4, 1] = np.nan
    
    In [49]: df.iloc[:2, 2] = np.nan
    
    In [50]: df
    Out[50]:
              0         1         2
    0  0.579400       NaN       NaN
    1 -2.226915       NaN       NaN
    2 -0.298148       NaN -0.598333
    3 -0.892747       NaN -0.987098
    4 -0.263859  1.235444 -1.338270
    5  1.020750  0.841896  0.341438
    6 -1.795927  0.716736 -2.130045
    
    In [51]: df.dropna()
    Out[51]:
              0         1         2
    4 -0.263859  1.235444 -1.338270
    5  1.020750  0.841896  0.341438
    6 -1.795927  0.716736 -2.130045
    
    In [52]: df.dropna(thresh=2)
    Out[52]:
              0         1         2
    2 -0.298148       NaN -0.598333
    3 -0.892747       NaN -0.987098
    4 -0.263859  1.235444 -1.338270
    5  1.020750  0.841896  0.341438
    6 -1.795927  0.716736 -2.130045

     

    결측치 채우기

    In [53]: df
    Out[53]:
              0         1         2
    0  0.579400       NaN       NaN
    1 -2.226915       NaN       NaN
    2 -0.298148       NaN -0.598333
    3 -0.892747       NaN -0.987098
    4 -0.263859  1.235444 -1.338270
    5  1.020750  0.841896  0.341438
    6 -1.795927  0.716736 -2.130045
    
    In [54]: df.fillna(0)
    Out[54]:
              0         1         2
    0  0.579400  0.000000  0.000000
    1 -2.226915  0.000000  0.000000
    2 -0.298148  0.000000 -0.598333
    3 -0.892747  0.000000 -0.987098
    4 -0.263859  1.235444 -1.338270
    5  1.020750  0.841896  0.341438
    6 -1.795927  0.716736 -2.130045
    
    # fillna에 딕셔너리 값을 넘기면 각 열마다 다른 값이 채워짐
    In [55]: df.fillna({1:0.5, 2: 0})
    Out[55]:
              0         1         2
    0  0.579400  0.500000  0.000000
    1 -2.226915  0.500000  0.000000
    2 -0.298148  0.500000 -0.598333
    3 -0.892747  0.500000 -0.987098
    4 -0.263859  1.235444 -1.338270
    5  1.020750  0.841896  0.341438
    6 -1.795927  0.716736 -2.130045
    
    # 재색인에서 사용 가능한 보간(interpolation) 메서드는 fillna 메서드에서도 사용 가능
    In [56]: df = pd.DataFrame(np.random.standard_normal((6, 3)))
    
    In [57]: df.iloc[2:, 1] = np.nan
    
    In [58]: df.iloc[4:, 2] = np.nan
    
    In [59]: df
    Out[59]:
              0         1         2
    0 -0.188093 -0.800852 -0.044144
    1  0.851879 -0.168139 -1.194898
    2 -0.547184       NaN  1.108437
    3  1.445043       NaN -0.906778
    4 -0.386706       NaN       NaN
    5  0.826388       NaN       NaN
    
    In [60]: df.fillna(method='ffill')
    Out[60]:
              0         1         2
    0 -0.188093 -0.800852 -0.044144
    1  0.851879 -0.168139 -1.194898
    2 -0.547184 -0.168139  1.108437
    3  1.445043 -0.168139 -0.906778
    4 -0.386706 -0.168139 -0.906778
    5  0.826388 -0.168139 -0.906778
    
    In [61]: df.fillna(method='ffill', limit=2)
    Out[61]:
              0         1         2
    0 -0.188093 -0.800852 -0.044144
    1  0.851879 -0.168139 -1.194898
    2 -0.547184 -0.168139  1.108437
    3  1.445043 -0.168139 -0.906778
    4 -0.386706       NaN -0.906778
    5  0.826388       NaN -0.906778
    
    # fillna로 평균값이나 중간값을 넘겨 데이터를 채울 수도 있음
    In [62]: data = pd.Series([1, np.nan, 3.5, np.nan,7])
    
    In [63]: data.fillna(data.mean())
    Out[63]:
    0    1.000000
    1    3.833333
    2    3.500000
    3    3.833333
    4    7.000000
    dtype: float64

     

    데이터 변형

     

    중복 제거

    In [64]: data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
        ...:                      'k2': [1, 1, 2, 3, 3, 4, 4]})
    
    In [65]: data
    Out[65]:
        k1  k2
    0  one   1
    1  two   1
    2  one   2
    3  two   3
    4  one   3
    5  two   4
    6  two   4
    
    # DataFrame의 duplicated 메서드는 각 행이 중복인지 아닌지를 알려주는 불리언 Series 객체 반환
    In [66]: data.duplicated()
    Out[66]:
    0    False
    1    False
    2    False
    3    False
    4    False
    5    False
    6     True
    dtype: bool
    
    In [67]: data.drop_duplicates()
    Out[67]:
        k1  k2
    0  one   1
    1  two   1
    2  one   2
    3  two   3
    4  one   3
    5  two   4

     

    중복을 찾아내기 위한 부분집합 따로 지정 가능

    In [68]: data['v1'] = range(7)
    
    In [69]: data
    Out[69]:
        k1  k2  v1
    0  one   1   0
    1  two   1   1
    2  one   2   2
    3  two   3   3
    4  one   3   4
    5  two   4   5
    6  two   4   6
    
    # k1 열에 기반해 중복 걸러내기
    In [70]: data.drop_duplicates(subset=['k1'])
    Out[70]:
        k1  k2  v1
    0  one   1   0
    1  two   1   1
    
    # keep='last' 옵션을 넘기면 마지막으로 발견된 값 반환
    In [71]: data.drop_duplicates(['k1', 'k2'], keep='last')
    Out[71]:
        k1  k2  v1
    0  one   1   0
    1  two   1   1
    2  one   2   2
    3  two   3   3
    4  one   3   4
    6  two   4   6
    
    In [72]: data.drop_duplicates(['k1', 'k2'])
    Out[72]:
        k1  k2  v1
    0  one   1   0
    1  two   1   1
    2  one   2   2
    3  two   3   3
    4  one   3   4
    5  two   4   5

     

    함수나 매핑을 이용해서 데이터 변형

    # DataFrame value 개수가 같아야 함!
    In [78]: data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon',
        ...:                               'pastrami', 'corned beef', 'bacon',
        ...:                               'pastrami', 'honey ham', 'nova lox'],
        ...:                      'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
    
    In [79]: data
    Out[79]:
              food  ounces
    0        bacon     4.0
    1  pulled pork     3.0
    2        bacon    12.0
    3     pastrami     6.0
    4  corned beef     7.5
    5        bacon     8.0
    6     pastrami     3.0
    7    honey ham     5.0
    8     nova lox     6.0
    
    # 원재료를 알려주는 열을 하나 추가하고 싶은 경우
    # 품목별 원재료를 담은 딕셔너리 데이터
    In [80]: meat_to_animal = {
        ...:     'bacon': 'pig',
        ...:     'pulled pork': 'pig',
        ...:     'pastrami': 'cow',
        ...:     'corned beef': 'cow',
        ...:     'honey ham': 'pig',
        ...:     'nova lox': 'salmon'
        ...: }
    
    # Series의 map 메서드는 변형을 위한 매핑 정보가 담긴 딕셔너리 같은 객체를 함수나 인수로 받음
    In [81]: data['animal'] = data['food'].map(meat_to_animal)
    
    In [82]: data
    Out[82]:
              food  ounces  animal
    0        bacon     4.0     pig
    1  pulled pork     3.0     pig
    2        bacon    12.0     pig
    3     pastrami     6.0     cow
    4  corned beef     7.5     cow
    5        bacon     8.0     pig
    6     pastrami     3.0     cow
    7    honey ham     5.0     pig
    8     nova lox     6.0  salmon

     

    함수 사용

    In [83]: def get_animal(x):
        ...:     return meat_to_animal[x]
        ...:
    
    In [84]: data['food'].map(get_animal)
    Out[84]:
    0       pig
    1       pig
    2       pig
    3       cow
    4       cow
    5       pig
    6       cow
    7       pig
    8    salmon
    Name: food, dtype: object
    
    
    
    In [85]: meat_to_animal['bacon']
    Out[85]: 'pig'
    
    In [86]: meat_to_animal
    Out[86]:
    {'bacon': 'pig',
     'pulled pork': 'pig',
     'pastrami': 'cow',
     'corned beef': 'cow',
     'honey ham': 'pig',
     'nova lox': 'salmon'}

     

    값 치환

    In [87]: data = pd.Series([1., -999., 2., -999., -1000., 3.])
    
    In [88]: data
    Out[88]:
    0       1.0
    1    -999.0
    2       2.0
    3    -999.0
    4   -1000.0
    5       3.0
    dtype: float64
    
    # -999는 누락된 데이터를 나타내는 감싯값, replace 메서드를 이용해
    # 판다스에서 인식할 수 있는 NA 값으로 치환해 새로운 Series 생성
    In [89]: data.replace(-999, np.nan)
    Out[89]:
    0       1.0
    1       NaN
    2       2.0
    3       NaN
    4   -1000.0
    5       3.0
    dtype: float64
    
    In [90]: data.replace([-999, -1000], np.nan)
    Out[90]:
    0    1.0
    1    NaN
    2    2.0
    3    NaN
    4    NaN
    5    3.0
    dtype: float64

     

    축 인덱스 이름 변경

    In [97]: data = pd.DataFrame(np.arange(12).reshape((3, 4)),
        ...:                     index=['Ohio', 'Colorado', 'New York'],
        ...:                     columns=['one', 'two', 'three', 'four'])
    
    In [98]: def transform(x):
        ...:     return x[:4].upper()
        ...:
    
    In [99]: data.index.map(transform)
    Out[99]: Index(['OHIO', 'COLO', 'NEW '], dtype='object')
    
    In [100]: data.index = data.index.map(transform)
    
    In [101]: data
    Out[101]:
          one  two  three  four
    OHIO    0    1      2     3
    COLO    4    5      6     7
    NEW     8    9     10    11
    
    # 원래 이름 변경하지 않고 새로운 객체를 생성하려면 rename 메서드 사용
    In [102]: data.rename(index=str.title, columns=str.upper)
    Out[102]:
          ONE  TWO  THREE  FOUR
    Ohio    0    1      2     3
    Colo    4    5      6     7
    New     8    9     10    11
    
    # 딕셔너리 형식의 객체를 이용해 축 이름 중 일부만 변경 가능
    In [103]: data.rename(index={'OHIO': 'INDIANA'},
         ...:             columns={'three': 'peekaboo'})
    Out[103]:
             one  two  peekaboo  four
    INDIANA    0    1         2     3
    COLO       4    5         6     7
    NEW        8    9        10    11

     

    이산화

    연속되는 데이터는 종종 개별로 분할하거나 분석을 위해 그룹으로 나누기도 함

    In [104]: ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
    
    In [105]: bins = [18, 25, 35, 60, 100]    # 범위
    
    In [106]: ages_categories = pd.cut(ages, bins)
    
    In [107]: ages_categories
    Out[107]:
    [(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
    Length: 12
    Categories (4, interval[int64, right]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]

    여기서 반환된 판다스 객체는 Categorical(범주형)이라는 특수한 객체, 결과는 pandas.cut으로 계산된 그룹

    각 그룹은 개별 그룹의 상한과 하한값을 담은 특수한 간격 값으로 구분

    In [108]: ages_categories.codes
    Out[108]: array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
    
    In [109]: ages_categories.categories
    Out[109]: IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]], dtype='interval[int64, right]')
    
    In [110]: ages_categories.categories[0]
    Out[110]: Interval(18, 25, closed='right')    # closed='right' 여기까지
    
    In [111]: pd.value_counts(ages_categories)    # pandas.cut 결과에 대한 그룹 개수
    Out[111]:
    (18, 25]     5
    (25, 35]     3
    (35, 60]     3
    (60, 100]    1
    Name: count, dtype: int64

    ( : 포함 x

    [ : 포함 o

    In [112]: ages
    Out[112]: [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
    
    In [113]: pd.cut(ages, bins, right=False)    # right=False를 전달해 소괄호와 대괄호 위치 변경
    Out[113]:
    [[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35, 60), [35, 60), [25, 35)]
    Length: 12
    Categories (4, interval[int64, left]): [[18, 25) < [25, 35) < [35, 60) < [60, 100)]
    
    In [114]: group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
    
    # labels 옵션으로 그룹의 이름을 리스트나 배열 형태로 직접 전달
    In [115]: pd.cut(ages, bins, labels=group_names)
    Out[115]:
    ['Youth', 'Youth', 'Youth', 'YoungAdult', 'Youth', ..., 'YoungAdult', 'Senior', 'MiddleAged', 'MiddleAged', 'YoungAdult']
    Length: 12
    Categories (4, object): ['Youth' < 'YoungAdult' < 'MiddleAged' < 'Senior']

     

    pandas.cut에 명시적으로 그룹의 경곗값을 넘기지 않고 그룹의 개수를 넘기면 데이터의 최솟값과 최댓값을 기준으로 균등한 길이의 그룹을 자동으로 계산

    In [116]: data = np.random.uniform(size=20)
    
    # precision=2 옵션으로 소수점 아래 두 자리까지로 제한
    In [117]: pd.cut(data, 4, precision=2)
    Out[117]:
    [(0.72, 0.95], (0.72, 0.95], (0.72, 0.95], (0.014, 0.25], (0.72, 0.95], ..., (0.25, 0.48], (0.72, 0.95], (0.014, 0.25], (0.72, 0.95], (0.48, 0.72]]
    Length: 20
    Categories (4, interval[float64, right]): [(0.014, 0.25] < (0.25, 0.48] < (0.48, 0.72] <
                                               (0.72, 0.95]]

    표본 사분위수(quartile)를 기반으로 데이터를 나누는 가장 적합한 함수 pandas.qcut

    pandas.cut 함수를 사용하면 데이터의 분산에 따라 각 그룹의 데이터 개수가 다르게 나뉘는 경우가 많음

    pandas.qcut은 표준 사분위수를 사용하므로 적당히 비슷한 크기의 그룹으로 나눌 수 있음

    In [118]: data = np.random.standard_normal(1000)
    
    In [119]: quartiles = pd.qcut(data, 4, precision=2)
    
    In [120]: quartiles
    Out[120]:
    [(-0.68, -0.0033], (0.73, 3.77], (-0.68, -0.0033], (-3.59, -0.68], (-0.68, -0.0033], ..., (-0.68, -0.0033], (-3.59, -0.68], (-3.59, -0.68], (-3.59, -0.68], (0.73, 3.77]]
    Length: 1000
    Categories (4, interval[float64, right]): [(-3.59, -0.68] < (-0.68, -0.0033] < (-0.0033, 0.73] <
                                               (0.73, 3.77]]
    
    In [121]: pd.value_counts(quartiles)
    Out[121]:
    (-3.59, -0.68]      250
    (-0.68, -0.0033]    250
    (-0.0033, 0.73]     250
    (0.73, 3.77]        250
    Name: count, dtype: int64
    
    # pandas.cut 함수처럼 사분위수(0부터 1까지의 값)를 직접 지정 가능
    In [122]: pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.]).value_counts()
    Out[122]:
    (-3.584, -1.3]       100
    (-1.3, -0.00334]     400
    (-0.00334, 1.348]    400
    (1.348, 3.773]       100
    Name: count, dtype: int64

     

    이상치를 찾고 제외

    이상치를 제외하거나 적당한 값으로 대체

    In [8]: data = pd.DataFrame(np.random.standard_normal((1000, 4)))
    
    In [9]: data.describe()
    Out[9]:
                     0            1            2            3
    count  1000.000000  1000.000000  1000.000000  1000.000000
    mean      0.060391     0.012253     0.001039    -0.015959
    std       0.951130     1.014482     0.955257     0.986806
    min      -2.764806    -2.633661    -3.151250    -3.146761
    25%      -0.563361    -0.727716    -0.646533    -0.653332
    50%       0.014199     0.012906     0.005474    -0.058561
    75%       0.719311     0.668322     0.650792     0.612658
    max       2.815886     4.433775     3.065100     3.432014
    
    In [10]: col = data[2]
    
    In [11]: col[col.abs() > 3]    # 2번 column에서 절댓값이 3을 초과하는 값
    Out[11]:
    0      3.065100
    187   -3.065525
    662   -3.151250
    Name: 2, dtype: float64
    
    In [12]: data
    Out[12]:
                0         1         2         3
    0   -0.965558  0.722591  3.065100 -0.613409
    1   -0.642368  0.691791  0.557011 -0.449008
    2   -0.220209  1.087734 -1.036212  1.503724
    3   -0.079624 -0.065160 -1.874135  0.076213
    4    0.010716 -1.982512  1.553767  1.668419
    ..        ...       ...       ...       ...
    995 -0.650527  0.479224 -0.347631  2.361066
    996 -0.396253 -1.436659  0.625960  0.417486
    997 -0.225310  0.559574  0.710567  1.129161
    998 -1.735939 -0.550929  1.098324 -1.191136
    999 -0.058288 -0.064661  0.333518  1.986984
    
    [1000 rows x 4 columns]
    
    In [13]: data[2]
    Out[13]:
    0      3.065100
    1      0.557011
    2     -1.036212
    3     -1.874135
    4      1.553767
             ...
    995   -0.347631
    996    0.625960
    997    0.710567
    998    1.098324
    999    0.333518
    Name: 2, Length: 1000, dtype: float64
    
    In [14]: data[2].abs()
    Out[14]:
    0      3.065100
    1      0.557011
    2      1.036212
    3      1.874135
    4      1.553767
             ...
    995    0.347631
    996    0.625960
    997    0.710567
    998    1.098324
    999    0.333518
    Name: 2, Length: 1000, dtype: float64
    
    In [15]:
    
    In [15]: data[2].abs() > 3
    Out[15]:
    0       True
    1      False
    2      False
    3      False
    4      False
           ...
    995    False
    996    False
    997    False
    998    False
    999    False
    Name: 2, Length: 1000, dtype: bool
    
    # 절댓값 3을 초과하는 값이 들어 있는 모든 행 선택
    # 불리언 DataFrame에서 any 메서드 사용
    In [16]: data[(data.abs() > 3).any(axis='columns')]
    Out[16]:
                0         1         2         3
    0   -0.965558  0.722591  3.065100 -0.613409
    172 -0.240364  4.433775 -0.525272 -1.842624
    187  0.172205 -0.043776 -3.065525 -0.342836
    509  1.248355 -1.150597 -1.270797 -3.146761
    662 -1.315525  0.465956 -3.151250  0.451275
    767 -0.387978 -0.864364  1.784505  3.075844
    975  1.185064 -0.276083  0.207537  3.432014

    비교 연산 결과에 any 메서드를 호출하려면 data.abs() > 3 구문을 괄호로 감싸야 함

    이 기준대로 쉽게 값 선택 가능, 다음 코드로 -3이나 3을 초과하는 값을 -3 또는 3으로 지정 가능

    In [17]: data[data.abs() > 3] = np.sign(data) * 3
    
    In [18]: data.describe()
    Out[18]:
                     0            1            2            3
    count  1000.000000  1000.000000  1000.000000  1000.000000
    mean      0.060391     0.010819     0.001191    -0.016320
    std       0.951130     1.009226     0.954354     0.984697
    min      -2.764806    -2.633661    -3.000000    -3.000000
    25%      -0.563361    -0.727716    -0.646533    -0.653332
    50%       0.014199     0.012906     0.005474    -0.058561
    75%       0.719311     0.668322     0.650792     0.612658
    max       2.815886     3.000000     3.000000     3.000000
    
    # np.sign(data)는 data가 양수인지 음수인지에 따라 1이나 -1이 담긴 배열 반환
    In [19]: np.sign(data).head()
    Out[19]:
         0    1    2    3
    0 -1.0  1.0  1.0 -1.0
    1 -1.0  1.0  1.0 -1.0
    2 -1.0  1.0 -1.0  1.0
    3 -1.0 -1.0 -1.0  1.0
    4  1.0 -1.0  1.0  1.0
    
    In [20]: np.sign(-3.4)
    Out[20]: -1.0
    
    In [21]: np.sign(3.4)
    Out[21]: 1.0

     

    뒤섞기와 임의 샘플링

    numpy.random.permutation 함수를 이용하면 Series나 DataFrame의 행을 임의의 순서대로 재배치

    순서를 바꾸고 싶은 만큼의 길이를 permutation 함수에 전달하면 순서가 바뀐 정수 배열 생성

    In [23]: df = pd.DataFrame(np.arange(5 * 7).reshape((5, 7)))
    
    In [24]: df
    Out[24]:
        0   1   2   3   4   5   6
    0   0   1   2   3   4   5   6
    1   7   8   9  10  11  12  13
    2  14  15  16  17  18  19  20
    3  21  22  23  24  25  26  27
    4  28  29  30  31  32  33  34
    
    In [25]: sampler = np.random.permutation(5)
    
    In [26]: sampler
    Out[26]: array([0, 2, 3, 4, 1])
    
    # 이 배열은 iloc 기반 index나 take 함수에서 사용 가능
    In [27]: df.take(sampler)
    Out[27]:
        0   1   2   3   4   5   6
    0   0   1   2   3   4   5   6
    2  14  15  16  17  18  19  20
    3  21  22  23  24  25  26  27
    4  28  29  30  31  32  33  34
    1   7   8   9  10  11  12  13
    
    In [28]: df.iloc[sampler]
    Out[28]:
        0   1   2   3   4   5   6
    0   0   1   2   3   4   5   6
    2  14  15  16  17  18  19  20
    3  21  22  23  24  25  26  27
    4  28  29  30  31  32  33  34
    1   7   8   9  10  11  12  13
    
    # take를 호출할 때 axis='columns'를 넘기면 열에 대해 작동
    In [29]: columns_sampler = np.random.permutation(7)
    
    In [30]: columns_sampler
    Out[30]: array([2, 6, 3, 0, 5, 4, 1])
    
    In [31]: df.take(columns_sampler, axis='columns')
    Out[31]:
        2   6   3   0   5   4   1
    0   2   6   3   0   5   4   1
    1   9  13  10   7  12  11   8
    2  16  20  17  14  19  18  15
    3  23  27  24  21  26  25  22
    4  30  34  31  28  33  32  29
    
    # 치환 없이 일부만 임의로 선택하고 싶은 경우(깥은 행 두 번 나타날 수 x) 
    # Series나 DataFrame의 sample 메서드 사용
    In [32]: df.sample(n=3)
    Out[32]:
        0   1   2   3   4   5   6
    0   0   1   2   3   4   5   6
    4  28  29  30  31  32  33  34
    1   7   8   9  10  11  12  13
    
    # 반복 선택을 허용하기 위해 치환을 통해 표본을 생성하려면 sample에 replace=True 옵션 전달
    In [33]: choices = pd.Series([5, 7, -1, 6, 4])
    
    In [34]: choices.sample(n=10, replace=True)
    Out[34]:
    4    4
    3    6
    1    7
    0    5
    4    4
    1    7
    3    6
    2   -1
    3    6
    1    7
    dtype: int64

     

    표시자, 더미 변수 계산

    통계 모델이나 머신러닝 애플리케이션을 위한 데이터 변환은 분류 값을 더미나 표시자 행렬로 전환하는 것

    ex) DataFrame의 한 열에 k가지 값이 있다면 k개의 열이 있는 DataFrame이나 행렬을 만들고 값으로 1과 0을 채워 넣을 것

    In [39]: df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
        ...:                    'data1': range(6)})
    
    In [40]: df
    Out[40]:
      key  data1
    0   b      0
    1   b      1
    2   a      2
    3   c      3
    4   a      4
    5   b      5
    
    In [41]: pd.get_dummies(df['key'], dtype=float)
    Out[41]:
         a    b    c
    0  0.0  1.0  0.0
    1  0.0  1.0  0.0
    2  1.0  0.0  0.0
    3  0.0  0.0  1.0
    4  1.0  0.0  0.0
    5  0.0  1.0  0.0
    
    # 표시자 DataFrame 열에 접두어(predix) 추가 후 다른 데이터와 병합하고 싶은 경우
    In [42]: pd.get_dummies(df['key'], prefix='key', dtype=float)
    Out[42]:
       key_a  key_b  key_c
    0    0.0    1.0    0.0
    1    0.0    1.0    0.0
    2    1.0    0.0    0.0
    3    0.0    0.0    1.0
    4    1.0    0.0    0.0
    5    0.0    1.0    0.0
    
    In [43]: dummies = pd.get_dummies(df['key'], prefix='key', dtype=float)
    
    In [44]: dummies
    Out[44]:
       key_a  key_b  key_c
    0    0.0    1.0    0.0
    1    0.0    1.0    0.0
    2    1.0    0.0    0.0
    3    0.0    0.0    1.0
    4    1.0    0.0    0.0
    5    0.0    1.0    0.0
    
    In [45]: df_with_dummy = df[['data1']].join(dummies)
    
    In [46]: df_with_dummy
    Out[46]:
       data1  key_a  key_b  key_c
    0      0    0.0    1.0    0.0
    1      1    0.0    1.0    0.0
    2      2    1.0    0.0    0.0
    3      3    0.0    0.0    1.0
    4      4    1.0    0.0    0.0
    5      5    0.0    1.0    0.0

     

    DataFrame의 한 행이 여러 범주에 속하는 경우 다른 접근 방식을 사용해 더미 변수를 만들어야 함

    In [7]: mnames = ['movie_id', 'title', 'genres']
    
    In [8]: movies = pd.read_table('movies.dat', sep='::',
       ...:                        header=None, names=mnames, engine='python')
    
    In [9]: movies
    Out[9]:
          movie_id                               title                        genres
    0            1                    Toy Story (1995)   Animation|Children's|Comedy
    1            2                      Jumanji (1995)  Adventure|Children's|Fantasy
    2            3             Grumpier Old Men (1995)                Comedy|Romance
    3            4            Waiting to Exhale (1995)                  Comedy|Drama
    4            5  Father of the Bride Part II (1995)                        Comedy
    ...        ...                                 ...                           ...
    3878      3948             Meet the Parents (2000)                        Comedy
    3879      3949          Requiem for a Dream (2000)                         Drama
    3880      3950                    Tigerland (2000)                         Drama
    3881      3951             Two Family House (2000)                         Drama
    3882      3952               Contender, The (2000)                Drama|Thriller
    
    [3883 rows x 3 columns]
    
    In [10]: movies[:10]
    Out[10]:
       movie_id                               title                        genres
    0         1                    Toy Story (1995)   Animation|Children's|Comedy
    1         2                      Jumanji (1995)  Adventure|Children's|Fantasy
    2         3             Grumpier Old Men (1995)                Comedy|Romance
    3         4            Waiting to Exhale (1995)                  Comedy|Drama
    4         5  Father of the Bride Part II (1995)                        Comedy
    5         6                         Heat (1995)         Action|Crime|Thriller
    6         7                      Sabrina (1995)                Comedy|Romance
    7         8                 Tom and Huck (1995)          Adventure|Children's
    8         9                 Sudden Death (1995)                        Action
    9        10                    GoldenEye (1995)     Action|Adventure|Thriller
    
    # str.get_dummies 메서드는 구분 문자열을 이용해 여러 그룹에 속하는 구성원 처리
    In [11]: dummies = movies['genres'].str.get_dummies('|')
    
    In [12]: dummies.iloc[:10, :6]
    Out[12]:
       Action  Adventure  Animation  Children's  Comedy  Crime
    0       0          0          1           1       1      0
    1       0          1          0           1       0      0
    2       0          0          0           0       1      0
    3       0          0          0           0       1      0
    4       0          0          0           0       1      0
    5       1          0          0           0       0      1
    6       0          0          0           0       1      0
    7       0          1          0           1       0      0
    8       1          0          0           0       0      0
    9       1          1          0           0       0      0
    
    # 이를 movies와 결합하고 add_prefix 메서드를 이용해 dummies의 열 이름에 'Genre_' 추가
    In [13]: dummies
    Out[13]:
          Action  Adventure  Animation  Children's  Comedy  Crime  ...  Mystery  Romance  Sci-Fi  Thriller  War  Western
    0          0          0          1           1       1      0  ...        0        0       0         0    0        0
    1          0          1          0           1       0      0  ...        0        0       0         0    0        0
    2          0          0          0           0       1      0  ...        0        1       0         0    0        0
    3          0          0          0           0       1      0  ...        0        0       0         0    0        0
    4          0          0          0           0       1      0  ...        0        0       0         0    0        0
    ...      ...        ...        ...         ...     ...    ...  ...      ...      ...     ...       ...  ...      ...
    3878       0          0          0           0       1      0  ...        0        0       0         0    0        0
    3879       0          0          0           0       0      0  ...        0        0       0         0    0        0
    3880       0          0          0           0       0      0  ...        0        0       0         0    0        0
    3881       0          0          0           0       0      0  ...        0        0       0         0    0        0
    3882       0          0          0           0       0      0  ...        0        0       0         1    0        0
    
    [3883 rows x 18 columns]
    
    In [14]: movies_windic = movies.join(dummies.add_prefix('Genre_'))
    
    In [15]: movies_windic
    Out[15]:
          movie_id                               title  ... Genre_War  Genre_Western
    0            1                    Toy Story (1995)  ...         0              0
    1            2                      Jumanji (1995)  ...         0              0
    2            3             Grumpier Old Men (1995)  ...         0              0
    3            4            Waiting to Exhale (1995)  ...         0              0
    4            5  Father of the Bride Part II (1995)  ...         0              0
    ...        ...                                 ...  ...       ...            ...
    3878      3948             Meet the Parents (2000)  ...         0              0
    3879      3949          Requiem for a Dream (2000)  ...         0              0
    3880      3950                    Tigerland (2000)  ...         0              0
    3881      3951             Two Family House (2000)  ...         0              0
    3882      3952               Contender, The (2000)  ...         0              0
    
    [3883 rows x 21 columns]
    
    In [16]: movies_windic.iloc[0]
    Out[16]:
    movie_id                                       1
    title                           Toy Story (1995)
    genres               Animation|Children's|Comedy
    Genre_Action                                   0
    Genre_Adventure                                0
    Genre_Animation                                1
    Genre_Children's                               1
    Genre_Comedy                                   1
    Genre_Crime                                    0
    Genre_Documentary                              0
    Genre_Drama                                    0
    Genre_Fantasy                                  0
    Genre_Film-Noir                                0
    Genre_Horror                                   0
    Genre_Musical                                  0
    Genre_Mystery                                  0
    Genre_Romance                                  0
    Genre_Sci-Fi                                   0
    Genre_Thriller                                 0
    Genre_War                                      0
    Genre_Western                                  0
    Name: 0, dtype: object

     

    pandas.get_dummies와 pandas.cut 같은 이산 함수를 조합해 통계 애플리케이션에서 사용

    In [20]: np.random.seed(12345)    # 난수가 반복 가능하도록 시드 값 고정
    
    In [21]: values = np.random.uniform(size=10)
    
    In [22]: values
    Out[22]:
    array([0.92961609, 0.31637555, 0.18391881, 0.20456028, 0.56772503,
           0.5955447 , 0.96451452, 0.6531771 , 0.74890664, 0.65356987])
    
    In [23]: bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
    
    In [24]: pd.get_dummies(pd.cut(values, bins))
    Out[24]:
       (0.0, 0.2]  (0.2, 0.4]  (0.4, 0.6]  (0.6, 0.8]  (0.8, 1.0]
    0       False       False       False       False        True
    1       False        True       False       False       False
    2        True       False       False       False       False
    3       False        True       False       False       False
    4       False       False        True       False       False
    5       False       False        True       False       False
    6       False       False       False       False        True
    7       False       False       False        True       False
    8       False       False       False        True       False
    9       False       False       False        True       False

     

    확장 데이터 유형

     

    판다스는 넘파이 기능 기반으로 만들어짐

    최근 판다스는 넘파이에서 기본적으로 지원하지 않는 자료형이더라도 사용할 수 있도록 하는 확장형 시스템 개발

      > 이 새로운 자료형은 넘파이 배열에서 가져오는 데이터와 동일하게 취급

    In [25]: s = pd.Series([1, 2, 3, None])
    
    In [26]: s
    Out[26]:
    0    1.0
    1    2.0
    2    3.0
    3    NaN
    dtype: float64
    
    In [27]: s.dtype
    Out[27]: dtype('float64')

    호환성을 위해 Series는 float64 자료형 사용, 누락된 값에 대해 np.nan 사용하는 레거시 방식을 따름

    pandas.Int64Dtype을 이용해 Series 생성 가능

    In [28]: s = pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype())
    
    In [29]: s
    Out[29]:
    0       1
    1       2
    2       3
    3    <NA>
    dtype: Int64
    
    In [30]: s.isna()
    Out[30]:
    0    False
    1    False
    2    False
    3     True
    dtype: bool
    
    In [31]: s.dtype
    Out[31]: Int64Dtype()

    <NA> 출력은 확장형 배열에 누락된 값이 있음을 나타냄

      > pandas.NA라는 특수한 감싯값 사용

    In [32]: s[3]
    Out[32]: <NA>
    
    In [33]: s[3] is pd.NA
    Out[33]: True
    
    In [34]: s[3] is np.nan
    Out[34]: False
    
    In [35]: s[3] is None
    Out[35]: False

     

    pd.Int64Dtype() 대신 'Int64'로 줄여서 사용 가능, 대소문자 구분하지 않으면 넘파이 기반의 비확장형으로 처리됨

    In [38]: s = pd.Series([1, 2, 3, None], dtype='Int64')
    
    In [39]: s
    Out[39]:
    0       1
    1       2
    2       3
    3    <NA>
    dtype: Int64
    
    # 판다스에는 넘파이 객체 배열을 사용하지 않는 문자열 데이터를 위한 특수한 확장형 존재
    # (별도로 설치해야 하는 pyarrow 라이브러리 필요)
    In [40]: s = pd.Series(['one', 'two', None, 'three'], dtype=pd.StringDtype())
    
    In [41]: s
    Out[41]:
    0      one
    1      two
    2     <NA>
    3    three
    dtype: string
    
    In [42]: s = pd.Series(['one', 'two', None, 'three'])
    
    In [43]: s
    Out[43]:
    0      one
    1      two
    2     None
    3    three
    dtype: object
    
    # Series의 astype 메서드에 확장형을 인수로 전달하면 데이터 정제 과정에서 손쉽게 변환 수행
    In [44]: df = pd.DataFrame({'A': [1, 2, None, 4],
        ...:                    'B': ['one', 'two', 'three', None],
        ...:                    'C': [False, None, False, True]})
    
    In [45]: df
    Out[45]:
         A      B      C
    0  1.0    one  False
    1  2.0    two   None
    2  NaN  three  False
    3  4.0   None   True
    
    In [46]: df['A'] = df['A'].astype('Int64')
    
    In [47]: df['B'] = df['B'].astype('string')
    
    In [48]: df['C'] = df['C'].astype('boolean')
    
    In [49]: df
    Out[49]:
          A      B      C
    0     1    one  False
    1     2    two   <NA>
    2  <NA>  three  False
    3     4   <NA>   True

    기본적으로 숫자는 float, 다른 형태는 object 형식

     

    문자열 다루기

     

    복잡한 패턴 매칭이나 텍스트 조작에는 정규 표현식이 필요

    판다스는 배열 데이터 전체에 쉽게 정규 표현식을 적용하고 누락된 데이터를 편리하게 처리하는 기능을 제공

     

    파이썬 내장 문자열 객체 메서드

    In [50]: val = 'a,b,  guido'
    
    # 쉼표로 구분된 문자열 split 메서드로 분리
    In [51]: val.split(',')
    Out[51]: ['a', 'b', '  guido']
    
    # 공백 문자(개행 문자 포함)를 제거하는 strip 메서드와 조합해 사용하기도 함
    In [52]: pieces = [x.strip() for x in val.split(',')]
    
    In [53]: pieces
    Out[53]: ['a', 'b', 'guido']
    
    # 더하기 연산을 이용해 :: 문자열과 합칠 수도 있음
    In [54]: first, second, third = pieces
    
    In [55]: first + '::' + second + '::' + third
    Out[55]: 'a::b::guido'
    
    # :: 문자열의 join 메서드에 리스트나 튜플을 전달하는 방법이 더 o
    In [56]: '=='.join(pieces)
    Out[56]: 'a==b==guido'

     

    일치하는 부분 문자열(substring)의 위치를 찾는 메서드도 있음, index나 find 대신 파이썬의 in 예약어 사용

    In [57]: 'guido' in val
    Out[57]: True
    
    In [58]: val.index(',')
    Out[58]: 1
    
    In [59]: val.find(':')
    Out[59]: -1
    
    In [60]: val.index(':')    # index의 경우 찾지 못하면 예외 발생
    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    Cell In[60], line 1
    ----> 1 val.index(':')
    
    ValueError: substring not found
    
    # count는 특정 부분 문자열이 몇 건 발견되었는지 반환
    In [61]: val.count(',')
    Out[61]: 2
    
    # replace는 찾아낸 패턴을 다른 문자열로 치환
    # 이 메서드에 비어 있는 문자열을 넘겨 패턴을 삭제하는 방법으로도 사용됨
    In [62]: val.replace(',', '::')
    Out[62]: 'a::b::  guido'
    
    In [63]: val.replace(',', '')
    Out[63]: 'ab  guido'
    
    In [64]: val.replace(' ', '')
    Out[64]: 'a,b,guido'

     

    정규 표현식

    텍스트에서 문자열 패턴을 찾는 유연한 방법 제공

    regex라 부르는 단일 표현식은 정규 표현식 언어로 구성된 문자열로 파이썬에 내장된 re 모듈이 문자열과 관련된 정규 표현식을 처리

     

    re 모듈 함수는 패턴 매칭, 치환, 분리 세 가지로 나눌 수 있음

    여러 가지 공백 문자(탭, 스페이스, 개행 문자)가 포함된 문자열을 나누고 싶다면 하나 이상의 공백 문자를 의미하는 \s+ 사용해 문자열 분리

    In [65]: import re
    
    In [66]: text = 'foo    bar\t baz    \tqux'
    
    # 공백이 하나 이상이면 분리
    In [67]: re.split(r'\s+', text)
    Out[67]: ['foo', 'bar', 'baz', 'qux']
    
    # re.split(r'\s+', text)를 사용하면 정규 표현식이 컴파일되고 그 다음 split 메서드가 실행됨
    # re.compile을 통해 직접 정규 표현식을 컴파일하고 얻은 정규 표현식 객체를 재사용할 수 있음
    In [68]: regex = re.compile(r'\s+')
    
    In [69]: regex.split(text)
    Out[69]: ['foo', 'bar', 'baz', 'qux']
    
    # 정규 표현식에 매칭되는 모든 패턴의 목록, findall 메서드
    In [70]: regex.findall(text)
    Out[70]: ['    ', '\t ', '    \t']
    
    In [71]: re.findall(r'\s+', text)
    Out[71]: ['    ', '\t ', '    \t']
    
    In [72]: re.findall('\s+', text)
    Out[72]: ['    ', '\t ', '    \t']
    
    In [73]: re.findall('\\s+', text)
    Out[73]: ['    ', '\t ', '    \t']

     

    동일한 정규 표현식을 다른 문자열에도 적용해야 할 경우 re.compile로 정규 표현식 객체를 만들어 사용하는 방법 추천(CPU 사용량 절약)

     

    match와 search는 findall 메서드와 밀접하게 관련되어 있음

      · findall은 문자열에서 일치하는 모든 부분 문자열을 찾음

      · search 메서드는 패턴과 일치하는 첫 번째 항목을 반환

      · match 메서드는 문자열의 시작 부분에서 일치하는 것만 찾음

     

    예제

    In [99]: text = """Dave dave@google.com
        ...: Steve steve@gmail.com
        ...: Rob rob@gmail.com
        ...: Ryan ryan@yahoo.com"""
    
    In [100]: pattern = r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}"
    
     # re.IGNORECASE는 정규 표현식이 대소문자를 구분하지 않도록 함
    In [101]: regex = re.compile(pattern, flags=re.IGNORECASE)
    
    # findall 메서드를 사용해 이메일 주소 리스트 생성
    In [102]: regex.findall(text)
    Out[102]: ['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']
    
    # search는 텍스트에서 첫 번째 이메일 주소만 찾음
    # 이전 정규 표현식에 대한 match 객체는 문자열에서 패턴이 위치하는 시작점과 끝점만 알려줌
    In [103]: m = regex.search(text)
    
    In [104]: m
    Out[104]: <re.Match object; span=(5, 20), match='dave@google.com'>
    
    In [105]: text[m.start():m.end()]
    Out[105]: 'dave@google.com'
    
    In [106]: print(regex.match(text))
    None
    
    # sub 메서드는 찾은 패턴을 주어진 문자열로 치환 후 새로운 문자열 반환
    In [107]: print(regex.sub("REDACTED", text))
    Dave REDACTED
    Steve REDACTED
    Rob REDACTED
    Ryan REDACTED
    
    # 이메일 주소를 찾는 동시에 각 이메일 주소를 사용자 이름, 도메인 이름, 도메인 접미사 세 가지
    # 컴포넌트로 나눠야 한다면 각 패턴을 괄호로 묶어줌
    In [108]: pattern = r"([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})"
    
    In [109]: regex = re.compile(pattern, flags=re.IGNORECASE)
    
    # match 객체를 이용하면 groups 메서드로 각 패턴 컴포넌트의 튜플을 얻을 수 있음
    In [110]: m = regex.match("wesm@bright.net")
    
    In [111]: m.groups()
    Out[111]: ('wesm', 'bright', 'net')
    
    # 패턴에 그룹이 존재하면 findall 메서드는 튜플 목록 반환
    In [112]: regex.findall(text)
    Out[112]:
    [('dave', 'google', 'com'),
     ('steve', 'gmail', 'com'),
     ('rob', 'gmail', 'com'),
     ('ryan', 'yahoo', 'com')]
    
    # sub 역시 마찬가지로 \1, \2 같은 특수한 기호를 사용해 각 패턴 그룹에 접근 가능
    # \1는 첫 번째로 찾은 그룹, \2는 두 번째로 찾은 그룹을 의미
    In [113]: print(regex.sub(r"Username: \1, Domain: \2, Suffix: \3", text))
    Dave Username: dave, Domain: google, Suffix: com
    Steve Username: steve, Domain: gmail, Suffix: com
    Rob Username: rob, Domain: gmail, Suffix: com
    Ryan Username: ryan, Domain: yahoo, Suffix: com

     

    판다스의 문자열 함수

    뒤죽박죽 섞인 데이터를 분석을 위해 정리하려면 문자열을 다듬고 정규화하는 작업이 필요

    문자열을 담은 열에 누락된 값이 있는 경우 복잡해짐

    In [114]: data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com',
         ...:         'Rob': 'rob@gmail.com', 'Wes': np.nan}
    
    In [115]: data = pd.Series(data)
    
    In [116]: data
    Out[116]:
    Dave     dave@google.com
    Steve    steve@gmail.com
    Rob        rob@gmail.com
    Wes                  NaN
    dtype: object
    
    In [117]: data.isna()
    Out[117]:
    Dave     False
    Steve    False
    Rob      False
    Wes       True
    dtype: bool

     

    문자열과 정규 표현식 메서드는 data.map을 사용해 각 값에 적용(lambda 혹은 다른 함수를 넘겨)할 수 있지만 NA 값을 만나면 실패

      > 이런 문제에 대처하기 위해 Series에는 NA 값을 건너뛰도록 하는 문자열 처리 메서드가 있음, Series의 str 속성을 이용

    # 각 이메일 주소가 gmail을 포함하고 있는지 str.contains로 검사
    # 연산 결과로 object dtype을 가짐
    In [118]: data.str.contains('gmail')
    Out[118]:
    Dave     False
    Steve     True
    Rob       True
    Wes        NaN
    dtype: object
    
    # 판다스는 문자열, 정수, 불리언 데이터를 특수하게 처리하는 확장형 제공
    In [119]: data_as_string_ext = data.astype('string')
    
    In [120]: data_as_string_ext
    Out[120]:
    Dave     dave@google.com
    Steve    steve@gmail.com
    Rob        rob@gmail.com
    Wes                 <NA>
    dtype: string
    
    In [121]: data_as_string_ext.str.contains('gmail')
    Out[121]:
    Dave     False
    Steve     True
    Rob       True
    Wes       <NA>
    dtype: boolean

     

    범주형 데이터

     

    판다스의 메모리 사용량을 줄이고 성능을 개선하는 방법

    # 하나의 열에서 특정 값이 반복되는 경우
    In [122]: values = pd.Series(['apple', 'orange', 'apple', 'apple'] * 2)
    
    In [123]: values
    Out[123]:
    0     apple
    1    orange
    2     apple
    3     apple
    4     apple
    5    orange
    6     apple
    7     apple
    dtype: object
    
    # 배열 내 유일한 값 추출 unique
    In [124]: pd.unique(values)
    Out[124]: array(['apple', 'orange'], dtype=object)
    
    # 특정 값이 얼마나 많이 존재하는지 확인 value_counts
    In [125]: pd.value_counts(values)
    Out[125]:
    apple     6
    orange    2
    Name: count, dtype: int64
    
    In [126]: values = pd.Series([0, 1, 0, 0] * 2)
    
    In [127]: dim = pd.Series(['apple', 'orange'])
    
    In [128]: values
    Out[128]:
    0    0
    1    1
    2    0
    3    0
    4    0
    5    1
    6    0
    7    0
    dtype: int64
    
    In [129]: dim
    Out[129]:
    0     apple
    1    orange
    dtype: object
    
    # take 메서드 사용해 Series에 저장된 원래 문자열 구하기
    In [130]: dim.take(values)
    Out[130]:
    0     apple
    1    orange
    0     apple
    0     apple
    0     apple
    1    orange
    0     apple
    0     apple
    dtype: object

    이러한 정수 표현을 범주형 또는 딕셔너리형 표기법

    별개의 값을 담은 배열 - 범주형, 딕셔너리형 또는 단계별 데이터

    이런 종류의 데이터 - Categorical 또는 범주형 데이터

    범주형 데이터를 가리키는 정숫값 - 범주 코드 또는 코드

     

    범주형 표현을 사용하면 분석 작업에서 엄청난 성능 향상, 범주 코드를 변경하지 않은 채로 범주형 데이터를 변형할 수 있음

    비교적 적은 연산으로 수행할 수 있는 변형

      · 범주형 데이터의 이름 변경

      · 기존 범주형 데이터의 순서를 바꾸지 않고 새로운 범주 추가

     

    판다스의 Categorical 확장형

    주로 문자열 데이터에서 유사한 값이 다수 존재하는 경우 데이터를 효과적으로 압축해 적은 메모리에서도 빠른 성능을 내는 기법

    In [131]: fruits = ['apple', 'orange', 'apple', 'apple'] * 2
    
    In [132]: N = len(fruits)
    
    In [133]: rng = np.random.default_rng(seed=12345)
    
    
    In [134]: df = pd.DataFrame({'fruit': fruits,
         ...:                    'basket_id': np.arange(N),
         ...:                    'count': rng.integers(3, 15, size=N),    # 3부터 14까지 임의의 값
         ...:                    'weight': rng.uniform(0, 4, size=N)},    # 값이 중복되지 않도록 uniform
         ...:                  columns=['basket_id', 'fruit', 'count', 'weight'])
    
    In [135]: df
    Out[135]:
       basket_id   fruit  count    weight
    0          0   apple     11  1.564438
    1          1  orange      5  1.331256
    2          2   apple     12  2.393235
    3          3   apple      6  0.746937
    4          4   apple      5  2.691024
    5          5  orange     12  3.767211
    6          6   apple     10  0.992983
    7          7   apple     11  3.795525
    
    # df['fruit']은 파이썬 문자열 객체의 배열 > 범주형 데이터로 변경
    In [136]: fruit_cat = df['fruit'].astype('category')
    
    In [137]: fruit_cat
    Out[137]:
    0     apple
    1    orange
    2     apple
    3     apple
    4     apple
    5    orange
    6     apple
    7     apple
    Name: fruit, dtype: category
    Categories (2, object): ['apple', 'orange']
    
    # .array 속성으로 접근할 수 있는 fruit_cat의 값은 pandas.Categorical 인스턴스
    In [138]: c = fruit_cat.array
    
    In [139]: type(c)
    Out[139]: pandas.core.arrays.categorical.Categorical
    
    # Categorical 객체에는 categories와 codes 속성이 있음
    In [140]: c.categories
    Out[140]: Index(['apple', 'orange'], dtype='object')
    
    In [141]: c.codes
    Out[141]: array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8)
    
    # codes 속성과 categories 속성 간의 매핑을 가져올 수 있음
    In [142]: dict(enumerate(c.categories))
    Out[142]: {0: 'apple', 1: 'orange'}
    
    # 변환 완료된 값을 대입해 DataFrame의 열을 범주형으로 변경
    In [143]: df['fruit'] = df['fruit'].astype('category')
    
    In [144]: df['fruit']
    Out[144]:
    0     apple
    1    orange
    2     apple
    3     apple
    4     apple
    5    orange
    6     apple
    7     apple
    Name: fruit, dtype: category
    Categories (2, object): ['apple', 'orange']
    
    # 파이썬 시퀀스에서 pandas.Categorical 직접 생성 가능
    In [145]: my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
    
    In [146]: my_categories
    Out[146]:
    ['foo', 'bar', 'baz', 'foo', 'bar']
    Categories (3, object): ['bar', 'baz', 'foo']
    
    # 기존에 정의된 범주와 범주 코드가 있다면 from_codes로 범주형 데이터 생성 가능
    In [147]: categories = ['foo', 'bar', 'baz']
    
    In [148]: codes = [0, 1, 2, 0, 0, 1]
    
    In [149]: my_cats_2 = pd.Categorical.from_codes(codes, categories)
    
    In [150]: my_cats_2
    Out[150]:
    ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
    Categories (3, object): ['foo', 'bar', 'baz']

    범주형으로 변경하는 경우 명시적으로 지정하지 않는 한 특정 순서 보장 x

      > 범주형 배열은 입력 데이터의 순서에 따라 순서가 다를 수 있음

      > from_codes를 사용하거나 다른 범주형 데이터 생성자를 이용하면 순서 지정 가능

    In [151]: ordered_cat = pd.Categorical.from_codes(codes, categories,
         ...:                                         ordered=True)
    
    In [152]: ordered_cat
    Out[152]:
    ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
    Categories (3, object): ['foo' < 'bar' < 'baz']

    코드에서 ['foo' < 'bar' < 'baz']는 foo, bar, baz의 순서를 갖는다는 의미

    # 순서가 없는 범주형 인스턴스는 as_ordered 메서드를 사용해 정렬 가능
    In [153]: my_cats_2.as_ordered()
    Out[153]:
    ['foo', 'bar', 'baz', 'foo', 'foo', 'bar']
    Categories (3, object): ['foo' < 'bar' < 'baz']

     

    범주형 데이터가 문자열일 필요는 없음, 범주형 배열은 변경이 불가능한 값이라면 어떤 자료형이라도 포함 가능

     

    Categorical 연산

    In [3]: rng = np.random.default_rng(seed=12345)
    
    In [4]: draws = rng.standard_normal(1000)
    
    In [5]: draws[:5]
    Out[5]: array([-1.42382504,  1.26372846, -0.87066174, -0.25917323, -0.07534331])
    
    In [6]: # 사분위수, 통계
    
    In [7]: bins = pd.qcut(draws, 4)
    
    In [8]: bins
    Out[8]:
    [(-3.121, -0.675], (0.687, 3.211], (-3.121, -0.675], (-0.675, 0.0134], (-0.675, 0.0134], ..., (0.0134, 0.687], (0.0134, 0.687], (-0.675, 0.0134], (0.0134, 0.687], (-0.675, 0.0134]]
    Length: 1000
    Categories (4, interval[float64, right]): [(-3.121, -0.675] < (-0.675, 0.0134] < (0.0134, 0.687] <
                                               (0.687, 3.211]]
                                               
    # qcut 함수의 labels 인수를 통해 직접 이름 지정
    In [9]: bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
    
    In [10]: bins
    Out[10]:
    ['Q1', 'Q4', 'Q1', 'Q2', 'Q2', ..., 'Q3', 'Q3', 'Q2', 'Q3', 'Q2']
    Length: 1000
    Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']
    
    In [11]: bins.codes[:10]
    Out[11]: array([0, 3, 0, 1, 1, 0, 0, 2, 2, 0], dtype=int8)
    
    # 이름 붙인 bins는 데이터의 시작과 끝값에 포함된 정보를 포함하지 않으므로 groupby 이용해
    # 요약 통계 추출
    In [12]: bins = pd.Series(bins, name='quartile')
    
    In [13]: results = (pd.Series(draws)
        ...:            .groupby(bins)
        ...:            .agg(['count', 'min', 'max'])
        ...:            .reset_index())
    
    In [14]: results
    Out[14]:
      quartile  count       min       max
    0       Q1    250 -3.119609 -0.678494
    1       Q2    250 -0.673305  0.008009
    2       Q3    250  0.018753  0.686183
    3       Q4    250  0.688282  3.211418
    
    # 결과의 quartile 열은 bins 순서를 포함한 원래 범주 정보 유지
    In [15]: results['quartile']
    Out[15]:
    0    Q1
    1    Q2
    2    Q3
    3    Q4
    Name: quartile, dtype: category
    Categories (4, object): ['Q1' < 'Q2' < 'Q3' < 'Q4']

     

    범주형을 활용한 성능 개선

    범주형을 사용해 성능과 메모리 사용률 개선

    In [17]: N = 10_000_000
    
    In [18]: labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))
    
    # labels 범주형 변환
    In [19]: categories = labels.astype('category')
    
    In [20]: labels.memory_usage(deep=True)
    Out[20]: 600000132
    
    In [21]: categories.memory_usage(deep=True)
    Out[21]: 10000544
    
    In [22]: %time _ = labels.astype('category')
    CPU times: total: 375 ms
    Wall time: 381 ms
    
    In [23]:
    
    In [23]: %timeit labels.value_counts()
    263 ms ± 1.82 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
    
    In [24]: %timeit categories.value_counts()
    36.4 ms ± 672 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

     

    Categorical 메서드

    범주형 데이터를 담은 Series는 특화된 문자열 메서드인 Series.str과 유사한 몇 가지 특수한 메서드 제공

    categories와 codes에 쉽게 접근 가능

    In [25]: s = pd.Series(['a', 'b', 'c', 'd'] * 2)
    
    In [26]: cat_s = s.astype('category')
    
    In [27]: cat_s
    Out[27]:
    0    a
    1    b
    2    c
    3    d
    4    a
    5    b
    6    c
    7    d
    dtype: category
    Categories (4, object): ['a', 'b', 'c', 'd']
    
    # cat을 통해 Categorical 메서드 접근
    In [28]: cat_s.cat.codes
    Out[28]:
    0    0
    1    1
    2    2
    3    3
    4    0
    5    1
    6    2
    7    3
    dtype: int8
    
    In [29]: cat_s.cat.categories
    Out[29]: Index(['a', 'b', 'c', 'd'], dtype='object')
    
    # 데이터의 실제 범주가 데이터에서 관측된 네 종류를 넘는다 가정했을 때
    # set_categories 메서드를 이용해 변경 가능
    In [32]: actual_categories = ['a', 'b', 'c', 'd', 'e']
    
    In [33]: cat_s2 = cat_s.cat.set_categories(actual_categories)
    
    In [34]: cat_s2
    Out[34]:
    0    a
    1    b
    2    c
    3    d
    4    a
    5    b
    6    c
    7    d
    dtype: category
    Categories (5, object): ['a', 'b', 'c', 'd', 'e']
    
    # 데이터에는 변함 x, 새로운 범주가 추가됨
    In [35]: cat_s.value_counts()
    Out[35]:
    a    2
    b    2
    c    2
    d    2
    Name: count, dtype: int64
    
    In [36]: cat_s2.value_counts()
    Out[36]:
    a    2
    b    2
    c    2
    d    2
    e    0
    Name: count, dtype: int64

    분석 과정에서 큰 DataFrame이나 Series를 한번 걸러내고 나면 실제로 데이터에는 존재하지 않는 범주가 남아 있을 수 있음

      > remove_unused_categories 메서드로 관측되지 않는 범주 제거

    In [37]: cat_s3 = cat_s[cat_s.isin(['a', 'b'])]
    
    In [38]: cat_s3
    Out[38]:
    0    a
    1    b
    4    a
    5    b
    dtype: category
    Categories (4, object): ['a', 'b', 'c', 'd']
    
    In [39]: cat_s3.cat.remove_unused_categories()
    Out[39]:
    0    a
    1    b
    4    a
    5    b
    dtype: category
    Categories (2, object): ['a', 'b']

     

    모델링을 위한 더미 변수 생성

    In [41]: cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')
    
    In [42]: pd.get_dummies(cat_s, dtype='int')
    Out[42]:
       a  b  c  d
    0  1  0  0  0
    1  0  1  0  0
    2  0  0  1  0
    3  0  0  0  1
    4  1  0  0  0
    5  0  1  0  0
    6  0  0  1  0
    7  0  0  0  1

     

    'Python' 카테고리의 다른 글

    그래프와 시각화  (1) 2023.11.28
    데이터 준비: 조인, 병합, 변형  (0) 2023.11.27
    데이터 로딩과 저장, 파일 형식  (0) 2023.11.20
    판다스  (0) 2023.11.14
    넘파이 기본: 배열과 벡터 연산  (0) 2023.11.07
Designed by Tistory.