목차



1. 구간 분할(cut)

cut은 연속형 변수의 구간 분할을 도와주는 함수입니다. 예를 들어 0~100 까지는 ‘A’그룹, 101~200까지는 ‘B’그룹 같은 방식으로 구간을 나누는 경우 사용합니다.

  • 기본 문법
pd.cut(x,           # 연속형 변수를 갖는 객체
       bins         # 나눌 구간을 갖는 리스트
       right = True # 오른쪽 닫힘 여부
       labels= None # 각 구간의 이름 부여
       include_lowest=False) # 최솟값 포함 여부

bins 옵션에서는 구간을 어떻게 나눌지 정합니다. 위에서 든 예시를 표현하자면 bins=[0,100,200]이 되겠습니다. 오른쪽 닫힘 여부를 선택하는 right 옵션이 있는데 이에 대해서는 열린 구간과 닫힌 구간의 개념으로 이해하시면 됩니다. 이상과 이하, 미만과 초과를 구분지어야 하기 때문에 있는 옵션입니다. 최솟값 포함 여부는 bins

시리즈 하나를 예시로 만들어서 cut을 사용해보겠습니다.

# 시리즈 생성
s1=Series([1,2,3,5,7,10])

# '1초과 5이하, 5초과 10이하'로 구간 나누기
pd.cut(s1,bins=[1,5,10])
Out: 
0            NaN
1     (1.0, 5.0]
2     (1.0, 5.0]
3     (1.0, 5.0]
4    (5.0, 10.0]
5    (5.0, 10.0]
dtype: category
Categories (2, interval[int64]): [(1, 5] < (5, 10]]

# '1이상 5이하, 5초과 10이하'로 구간 나누기(최솟값 포함)
pd.cut(s1,bins=[1,5,10], include_lowest=True)
Out: 
0    (0.999, 5.0]
1    (0.999, 5.0]
2    (0.999, 5.0]
3    (0.999, 5.0]
4     (5.0, 10.0]
5     (5.0, 10.0]
dtype: category
Categories (2, interval[float64]): [(0.999, 5.0] < (5.0, 10.0]]

# '1이상 5이하는 A, 5초과 10이하는 B로 라벨링'
pd.cut(s1,bins=[1,5,10], include_lowest=True, labels = ['A','B'])
Out: 
0    A
1    A
2    A
3    A
4    B
5    B
dtype: category
Categories (2, object): [A < B]



2. 그룹 메소드(groupby)

groupby 특정 컬럼의 값 혹은 인덱스를 그룹화하는 메소드입니다. 이미 SQL에서 봤던 GROUP BY 절과 비슷합니다. 단, groupby만 수행시 분리만 수행하고 출력하는 것이 없기 때문에 적용함수를 같이 입력해야합니다.

아래 예제 파일을 사용하여 간단한 예제를 실행해봅시다.

emp.csv

# 아무것도 출력되지 않음
emp.groupby('DEPTNO')

# 부서번호로 그룹화하여 부서별 합계
emp.groupby('DEPTNO').sum()
Out: 
        EMPNO      MGR    SAL    COMM
DEPTNO                               
10      23555  15621.0   8750     0.0
20      38501  38661.0  10875     0.0
30      46116  46329.0   9400  2200.0
# 연산할 컬럼을 정하지 않아 모든 숫자 컬럼에 그룹함수 적용

# 부서번호로 그룹화하여 부서별 연봉 합계(시리즈 출력)
emp.groupby('DEPTNO')['SAL'].sum()
Out: 
DEPTNO
10     8750
20    10875
30     9400
Name: SAL, dtype: int64

# 부서번호로 그룹화하여 부서별 연봉 합계(데이터프레임 출력)
emp.groupby('DEPTNO')[['SAL']].sum()
Out[11]: 
          SAL
DEPTNO       
10       8750
20      10875
30       9400

# 부서번호로 그룹화하여 부서별 연봉 및 COMM 합계(데이터프레임 출력)
emp.groupby('DEPTNO')['SAL','COMM'].sum()
# emp.groupby('DEPTNO')[['SAL','COMM']].sum()
          SAL    COMM
DEPTNO               
10       8750     0.0
20      10875     0.0
30       9400  2200.0

​ 예시대로 실행해보면 아시겠지만 groupby 메소드에는 항상 그룹 함수가 수반되어야 합니다. 그리고 한 컬럼에만 그룹 함수 사용시, 대괄호 갯수에 따라서 시리즈로 출력될지 데이터 프레임으로 출력될지가 달라지기 때문에 이 부분에서도 주의해야합니다. 다른 함수에 다시 사용해야하는 경우 데이터 구조가 달라서 연산에 오류가 생기는 경우가 있기 때문입니다.

그리고 같은 컬럼에 대해서 함수 여러 개를 동시에 사용할 수도 있습니다. agg 메소드를 이용하는 방법입니다. agg의 괄호 안에 리스트 형태로 그룹 함수를 전달하면 순서대로 그룹 별 함수 결과 값을 출력합니다.

emp.groupby('DEPTNO')[['SAL']].agg(['sum','mean'])
Out: 
          SAL             
          sum         mean
DEPTNO                    
10       8750  2916.666667
20      10875  2175.000000
30       9400  1566.666667

groupby와 같이 사용할 그룹 함수는 앞에서와 비슷하게 사칙연산외에도 적용 메소드(apply)를 이용하여 사용자 정의 함수를 사용할 수도 있습니다.

1) 적용 메소드(apply)

예시 : sum을 적용 메소드를 이용하여 사용자 정의 함수 형태로 groupby에 전달

f1 = lambda x : x.sum()
emp.groupby('DEPTNO')['SAL'].apply(f1)
# = emp.groupby('DEPTNO')['SAL'].sum()

예시 : 연봉 기준으로 상위 2명 추출

f_sort = lambda x : x.sort_values('SAL', ascending=False)[:2]

# SAL이 큰 순서대로 전체에서 두 명 출력

emp.sort_values('SAL',ascending=False)[:2]
emp.sort_values('SAL',ascending=False).iloc[:2, :]

# 부서별로 SAL이 큰 순서대로 두 명 출력

emp.groupby('DEPTNO').apply(f_sort)
Out:
           EMPNO  ENAME        JOB  ...   SAL   COMM  DEPTNO
DEPTNO                              ...                     
10     8    7839   KING  PRESIDENT  ...  5000    NaN      10
       6    7782  CLARK    MANAGER  ...  2450    NaN      10
20     7    7788  SCOTT    ANALYST  ...  3000    NaN      20
       12   7902   FORD    ANALYST  ...  3000    NaN      20
30     5    7698  BLAKE    MANAGER  ...  2850    NaN      30
       1    7499  ALLEN   SALESMAN  ...  1600  300.0      30

[6 rows x 8 columns]

emp.groupby('DEPTNO', group_keys=False).apply(f_sort)
Out: 
    EMPNO  ENAME        JOB     MGR         HIREDATE   SAL   COMM  DEPTNO
8    7839   KING  PRESIDENT     NaN  1981-11-17 0:00  5000    NaN      10
6    7782  CLARK    MANAGER  7839.0  1981-06-09 0:00  2450    NaN      10
7    7788  SCOTT    ANALYST  7566.0  1987-04-17 0:00  3000    NaN      20
12   7902   FORD    ANALYST  7566.0  1981-12-03 0:00  3000    NaN      20
5    7698  BLAKE    MANAGER  7839.0  1981-05-01 0:00  2850    NaN      30
1    7499  ALLEN   SALESMAN  7698.0  1981-02-20 0:00  1600  300.0      30

※ group_keys=False 옵션의 사용 목적

groupby 연산 결과와 groupby 컬럼이 중복될 경우에 종종 key 중복으로 groupby 연산 자체가 에러 발생할 수 있습니다. 이런 경우에 이 옵션을 사용하면 groupby 연산 컬럼의 index로의 출력을 방지하여 출력할 수 있습니다.

다음으로는 컬럼 설정 외에 다른 방법으로 그룹화하는 방식에 대해서 설명하겠습니다.

2) 사용자 정의 그룹

groupby의 그룹 전달 방식은 지정한 컬럼에 들어있는 Value를 순서대로 지정하는 방식입니다. 그렇기 때문에 groupby의 괄호 내에 본인이 원하는 방식을 지정한다면 그 방식으로 그룹화를 실행할 수 있습니다. 원하는 방식으로 지정할 때 그 값을 행별로 하나하나 입력하여 리스트로 전달하는 방법도 있지만 가장 위에서 배운 cut 함수를 사용하는 방법도 있습니다. 두 방법 모두 다뤄보겠습니다.

예제 - 리스트로 그룹 전달 후 sum 함수 실행

# 데이터 프레임 생성
df1 = DataFrame(np.arange(1,26).reshape(5,5),
                columns = ['a','b','c','d','e'])

# 행별(axis = 0)
df1.groupby(['g1','g1','g2','g2','g2']).sum()
Out:
     a   b   c   d   e
g1   7   9  11  13  15
g2  48  51  54  57  60

# 열별
df1.groupby(['g1','g1','g2','g2','g2'], axis = 1).sum()
Out: 
   g1  g2
0   3  12
1  13  27
2  23  42
3  33  57
4  43  72

앞의 두 행(열)은 g1이 부여되고 뒤에 있는 세 행(열)은 g2가 부여되어 두 그룹으로 나누어지고 각 그룹 내에서 함수가 실행된 것을 확인할 수 있습니다.

리스트 방식 외에 딕셔너리 형태로 전달할 수도 있습니다. 리스트 전달과 달리 순서에 상관없이 전달이 가능하다는 이점이 있습니다. 물론 Key-Value 구조로 입력해야하기 때문에 코드 작성 속도는 느려집니다.

df1.groupby({'a':'g1', 'b':'g1', 'c':'g2',
             'd':'g2', 'e':'g2'}, axis = 1).sum()

다음으로는 cut을 이용하여 만든 cutting object 전달하여 그룹화하는 방법이 있습니다.위에서 다운로드 받은 emp.csv에서 각 연봉의 등급별 연봉의 평균을 출력해봅시다.

단, 연봉의 등급 기준은 3000이상 A, 1500 이상 3000미만 B, 1500미만 C입니다.

c1 = pd.cut(emp['SAL'],
            [0,1500,3000,10000],
            right=False,
            labels=['C','B','A'])

emp.groupby(c1)['SAL'].mean()

3) 트랜스폼(transform)

transform 메소드는 치환 메소드가 아니라 R에서 'plyr' 패키지 내에 함수 'ddply'에 있는 transform 과 기능이 유사합니다. 기존에 사용한 그룹 함수들은 그룹별 함수 실행 결과만 출력하는 반면에 transform을 이용하면​ 모든 행에 그룹 함수의 결과값을 붙여서 보여줍니다.

# 부서별 평균
emp.groupby('DEPTNO')['SAL'].mean()
Out:
DEPTNO
10 2916.666667
20 2175.000000
30 1566.666667

# 부서별 평균 (with transform)
emp.groupby('DEPTNO')['SAL'].transform('mean')
Out :
0  2175.000000
1  1566.666667
2  1566.666667
3  2175.000000
4  1566.666667
5  1566.666667
6  2916.666667
7  2175.000000
8  2916.666667
9  1566.666667
10 2175.000000
11 1566.666667
12 2175.000000
13 2916.666667



3. 날짜 모듈(datetime)

날짜 관련 모듈 datetime은 날짜, 시간 관련 모듈입니다.

from datetime import datetime

1) 현재 날짜 및 시간 확인

# 현재 시간 출력 및 변수화
d1 = datetime.now(); d1
Out: datetime.datetime(2020, 10, 3, 15, 19, 27, 485830)

# 년, 월, 일 출력
d1.year
Out: 2020

d1.month
Out: 10

d1.day
Out: 3

# 시, 분, 초 출력
d1.hour
Out: 15

d1.minute
Out : 19

d1.second
Out : 23

2) 형 변환 함수

날짜에서의 형 변환 함수에는 문자를 날짜로, 날짜를 문자로 바꾸는 함수가 있습니다. 데이터 형이 맞지 않으면 연산시 에러가 발생하기 때문에 날짜 데이터를 다룰 경우에 필요한 함수입니다.

1) strptime

문자를 날짜 형식으로 치환하는 함수로 메소드는 존재하지 않습니다. 벡터 연산은 불가능하기 때문에 map 등 벡터 연산 지원 함수를 같이 사용해아합니다.

# 날짜 변수와 리스트 생성 / 함수 사용전에는 문자 형
a1 = '2011/01/11'
L1 = ['2011/01/11','2012/12/31']

# 변수를 날짜 형으로 변환
v1 = datetime.strptime(a1, '%Y/%m/%d') 
Out:
datetime.datetime(2011, 1, 11, 0, 0)

# 메소드 형식 불가
a1.strptime('%Y/%m/%d')

# 벡터 연산 불가 error
datetime.strptime(L1, '%Y/%m/%d')

# 리스트 내포 표현식 이용
[datetime.strptime(i, '%Y/%m/%d') for i in L1]
Out : 
[datetime.datetime(2011, 1, 11, 0, 0), datetime.datetime(2012, 12, 31, 0, 0)]

2) strftime :

날짜 형식을 문자 형식으로 치환하는 기능을 가지고 있으며 메소드, 함수 형식 모두 가능합니다. strptime과 마찬가지로 벡터 연산은 불가능하기 때문에 map 등 벡터 연산 지원 함수를 같이 사용해아합니다.

# 함수, 메소드 형식 모두 가능
v1.strftime('%A')
datetime.strftime(v1,'%A')
Out: 'Tuesday'

# 벡터 연산 불가
datetime.strftime(v2,'%A')

저번 글에서 다룬 datetime 모듈에 대한 추가 내용입니다. 날짜 관련 연산 및 인덱스 생성에 대해서 작성하고자 합니다.

3) 날짜 연산

# 날짜 두 개 생성
d1 = datetime.now(); d1
datetime.datetime(2020, 10, 3, 15, 19, 27, 485830)
d2 = datetime.strptime('2020-10-30','%Y-%m-%d')

# 단순 뺄셈
d2 - d1
Out: datetime.timedelta(days=26, seconds=31232, microseconds=514170)

# 날짜 차이
(d2 - d1).days
Out: 26

# 초 차이
(d2-d1).seconds
Out: 31232

# 날짜 연산 ??
d2 + 1;
Traceback (most recent call last):

  File "<ipython-input-38-0351a7bf6e76>", line 1, in <module>
    d2 + 1

TypeError: unsupported operand type(s) for +: 'datetime.datetime' and 'int'

(+) 추가 모듈을 사용한 날짜 연산

1) timedelta

datetime 형의 값 끼리는 연산시 문제가 없지만 날짜를 하루 씩 더하거나 하려고 하면 데이터 형 문제로 인해 연산이 바로 수행되지 않습니다. 이를 위해 datetime 내에는 timedelta와 offset이라는 모듈이 들어있습니다. timedelta는 단위 하나가 하루를 의미합니다. 그러므로 7일은 timedelta(7)이 되고 1시간은 timedelta(1/24)가 됩니다.

from datetime import timedelta

# 7일 추가
d1 + timedelta(7) 
Out: datetime.datetime(2020, 10, 10, 15, 19, 27, 485830)

# 1시간 추가
d1 + timedelta(1/24) 
Out[42]: datetime.datetime(2020, 10, 3, 16, 19, 27, 485830)

​ ​

2) offset

# 모듈 불러오기
import pandas.tseries.offsets
from pandas.tseries.offsets import Day, Hour, Second

d1 + Day(5) 
Out: Timestamp('2020-10-08 15:19:27.485830')

4) 날짜 인덱스

인덱스에 날짜를 입력하는 date_range입니다. 시작 날짜와 끝 날짜를 입력하면 기본적으로 일 단위로 날짜를 입력하며 주 단위로 할지, 년 단위로 할지도 설정할 수 있습니다. ​ 기본 문법

pd.date_range(start = "",   # 시작
              end=None ,    # 끝
              periods = "", # 구간 갯수
              freq = None)  # 날짜 생성 단위

# 2020년 9월 1일부터 30일까지 날짜 인덱스 생성
pd.date_range('2020/09/01', '2020/09/30')
Out: 
DatetimeIndex(['2020-09-01', '2020-09-02', '2020-09-03', '2020-09-04',
               '2020-09-05', '2020-09-06', '2020-09-07', '2020-09-08',
               '2020-09-09', '2020-09-10', '2020-09-11', '2020-09-12',
               '2020-09-13', '2020-09-14', '2020-09-15', '2020-09-16',
               '2020-09-17', '2020-09-18', '2020-09-19', '2020-09-20',
               '2020-09-21', '2020-09-22', '2020-09-23', '2020-09-24',
               '2020-09-25', '2020-09-26', '2020-09-27', '2020-09-28',
               '2020-09-29', '2020-09-30'],
              dtype='datetime64[ns]', freq='D')

# 같은 범위에서 균등한 날짜 구간 10개 생성
pd.date_range('2020/09/01', '2020/09/30', periods = 10)
Out: 
DatetimeIndex(['2020-09-01 00:00:00', '2020-09-04 05:20:00',
               '2020-09-07 10:40:00', '2020-09-10 16:00:00',
               '2020-09-13 21:20:00', '2020-09-17 02:40:00',
               '2020-09-20 08:00:00', '2020-09-23 13:20:00',
               '2020-09-26 18:40:00', '2020-09-30 00:00:00'],
              dtype='datetime64[ns]', freq=None)

# 같은 범위에서 주 단위로 날짜 생성(기본값 : 일요일)
pd.date_range('2020/09/01', '2020/09/30', freq = 'W')
Out:
DatetimeIndex(['2020-09-06', '2020-09-13', '2020-09-20', '2020-09-27'], dtype='datetime64[ns]', freq='W-SUN')


​(+) 날짜 인덱스 색인

시리즈나 데이터프레임에서 날짜 인덱스를 지정 후 특정 기간에 대한 간단한 색인이 가능합니다. 예를 들어 시리즈 s1에서 2020년 1월 내에 모든 자료를 보고 싶을 때는 loc를 이용한 범위 설정 필요 없이 s1['2020-01']을 입력하면 됩니다. 아래 예시를 보면서 확인해봅시다.

# 2020년 주 단위 인덱스 생성
d1 = pd.date_range('2020/01/01','2020/12/31',freq='W')
# 인덱스는 d1, Value가 1부터 시작되는 시리즈 생성
s1 = Series(np.arange(1, len(d1)+1), index=d1)

s1['2020-01']
Out[58]: 
2020-01-05    1
2020-01-12    2
2020-01-19    3
2020-01-26    4
Freq: W-SUN, dtype: int32

5) 리샘플링(resample)

resample은 날짜의 빈도수를 변경하는 메소드입니다. 위에서 배운 date_range로 생성된 날짜 인덱스의 단위(freq)를 수정할 수가 있는데요. 기존 날짜 단위보다 더 세밀하게 바꿔주는(갯수 증가) 업 샘플링(upsampling)이 있고 기존 날짜 단위보다 간격이 넓어지는(갯수 감소) 다운 샘플링(downsampling)이 있습니다.

# upsampling : (주별-> 일별)
s1.resample('D').ffill()
Out: 
2020-01-05     1
2020-01-06     1
2020-01-07     1
2020-01-08     1
2020-01-09     1
              ..
2020-12-23    51
2020-12-24    51
2020-12-25    51
2020-12-26    51
2020-12-27    52
Freq: D, Length: 358, dtype: int32

# downsampling : (일별 -> 월별)
s1.resample('M').sum()
Out: 
2020-01-31     10
2020-02-29     26
2020-03-31     55
2020-04-30     62
2020-05-31    100
2020-06-30     98
2020-07-31    114
2020-08-31    165
2020-09-30    150
2020-10-31    166
2020-11-30    230
2020-12-31    202
Freq: M, dtype: int32


여기서 업 샘플링시 주의할 점이 있습니다. 주 단위에서 일 단위로 수정을 하였는데 1월 1일부터가 아니라 1월 5일부터인 것을 볼 수 있습니다.​ 처음 날짜 인덱스 생성시 초깃값이 1월 5일이기 때문입니다. 그래서 업 샘플링을 실행한 경우에는 본인이 원하는 형식으로 인덱스가 바뀌었는지 재확인 해볼 필요가 있습니다.

6) 날짜 이동 (shift)

shift는 날짜 인덱스를 고정 후 값만 이동시키는 메소드입니다.

기본 문법

d1.shift(periods = 1, # 이동범위
         freq = None, # 기간
         axis = 0,    # 축(기본값 : 행)
         fill_value)  # 이전 값 가져올 수 없는 경우 NA리턴

# 시리즈에 shift 사용 예
s1.shift(periods = 1)
Out: 
2020-01-05     NaN
2020-01-12     1.0
2020-01-19     2.0
2020-01-26     3.0
2020-02-02     4.0
2020-02-09     5.0
...
Freq: W-SUN, dtype: float64

Python에서의 전처리 관련 내용은 다음 글에서 마칠 예정입니다. 추가 문제에 대한 글 또한 예정되어 있습니다만, R에 대한 복습이 필요하여 전처리 외의 추가적인 내용은 R에 대한 내용 이후 다루려고 합니다.