Home » Pandas 기초 사용법 1

Pandas 기초 사용법 1

Pandas는 1차원 열 데이터 및 2차원의 표형식 데이터를 사용하는 파이썬용 데이터 분석 라이브러리이다. 여기서 핵심이 되는 데이터 유형은 1차원 데이터를 다룰 때 사용하는 Series와 표 형식의 데이터를 다룰 때 사용하는 DataFrame이다.

Series

Series는 배열과 유사한 1차원 데이터를 위한 자료 구조이다. 파이썬 배열과 달리 계열 내의 모든 데이터는 동일한 값을 가져야 한다. 물론 모든 파이썬 값은 ‘파이썬 객체’라는 모호한 타입으로 다뤄질 수 있으나, numpy의 배열과 마찬가지로 효율이나 성능을 위해서는 메모리 상의 크기가 고정된 원시타입 값을 사용하는 것이 좋다. numpy 배열과 유사하지만, numpy 배열이 정수값만을 인덱스로 사용하는데 반해 Series는 별도의 인덱스를 가질 수 있어서 정수값 뿐만 아니라 인덱스를 통해서 데이터에 액세스할 수 있다.

생성하기

np.Series()를 사용해서 계열 데이터를 생성할 수 있다. 파이썬 리스트를 사용할 수도 있으나, 보통은 numpy 배열을 주로 사용한다.

> pd.Series([1,2,3, 4])
> pd.Series([1,2,3,4], index=list('ABCD')

파이썬의 정수 리스트를 데이터로 주면 이는 int64 타입으로 변환되어 저장된다. index= 옵션을 주면 해당 리스트/배열을 인덱스로 사용할 수 있다. pd.date_range()를 사용하면 시계열 인덱스를 생성할 수 있어서 다음과 같은 식으로 시계열 난수 데이터를 생성해볼 수 있다.

> pd.Series(np.random.randn(100),
            index=pd.date_range('2020-01-01 00:00:00', freq='1S', periods=100))

액세스

기본적인 사용법은 리스트나 numpy 배열과 유사하다. 인덱스가 지정되어 있더라도 위치값(정수)을 사용해서 특정 원소를 액세스하거나 슬라이스할 수 있다.

xr = pd.Series(np.random.randn(4), index=list('ABCD'))
# 위치, 인덱스로 액세스
xr[0]    # 위치로 액세스
xr['A']  # 인덱스값으로 액세스
xr.A     # 인덱스는 속성처럼 액세스할 수 있다.
# 슬라이스로 액세스
xr[1:3]  # 위치 슬라이스
xr['B':'D'] # 인덱스 슬라이스
# *** 인덱스 슬라이스는 오른쪽 끝을 포함한다.
# 인덱스, 위치의 리스트
xr[[1, 3]] # -> 2, 4 번째 원소
xr[['A', 'D']]
# bool 값의 리스트로 필터링
xr[[True, False, False, True]]

통계 기능

Series는 그 자체로 간단한 통계 기능을 제공한다. 평균, 합계, 누적합, 중간값, 사분위수 등을 계산할 수 있다.

# 평균
xr.mean()
# 중간값
xr.median()
# 4분위수
xr.quantile([.25, .5, .75])
# 누계
xr.cumsum()
# 표본표준편차
xr.std()
# 모표준편차
xr.std(ddof=0)
# 표본분산
xr.var()
# 모분산
xr.var(ddof=0)

numpy 배열 역시 분산, 표준편차를 계산하는 메소드를 제공하는데, 주의할 점은 pandas에서는 기본적으로 표본분산, 표본표준편차를 계산한다는 차이가 있다는 것이다. numpy와 동일한 계산 결과를 얻으려면 ddof=0을 인자로 전달해야 한다.

그래프 기능

pandas는 시각화 라이브러리가 설치되어 있는 경우, 이를 사용하여 Series의 내용을 바로 플롯차트로 그려보는 기능도 제공한다. Series는 별도의 인덱스를 가지고 있으므로 그 단위 하나만으로도 그래프를 그릴 수 있다.

from matplotlib import pyplot as plt
import numpy as np
import pandas as pd
sr = pd.Series(np.random.randn(100),
        index=pd.date_range('2020-08-01', freq='D', periods=100))
plt.figure()
sr.plot()
plt.show()


데이터 프레임에서 기본적인 데이터 선택이나 조작에 대해서는 Pandas 공식 홈페이지에 튜토리얼 문서가 잘 구비되어 있다. 이를 김석준님이 번역하고 코멘트한 포스팅이 있으므로 참고하자. (이 글보다 훨씬 더 자세하고 친절하다.👍👍👍)

데이터 프레임 만들기

데이터 프레임을 만드는 방법에는 여러 가지가 있지만, 먼저 Series를 쌓는 것부터 시작하는 것이 좋겠다. 왜냐하면 이게 엄청 헷갈리기 때문이다. Series는 한 방향으로만 데이터가 나열되며, 각각의 데이터는 고유의 인덱스를 가진다고 했다.

데이터 프레임은 이 Series가 ‘적층된’ 형식의 데이터이다. 따라서 행과 열이 각각 존재한다. 데이터 프레임의 열을 따라서는 column 으로 구분하며, 행을 따라서는 index가 또 존재한다. 데이터프레임과 Series가 모두 index를 가지므로 Series의 index가 데이터 프레임의 인덱스가 된다고 생각하기 쉬운데, 실제로는 그렇지 않다.

Series의 리스트를 데이터프레임으로 변환하면 각각의 Series 값은 데이터프레임의 한 행이 된다. 그리고 데이터 프레임의 인덱스는 별도로 정의할 수 있다.

sa = pd.Series(np.random.random(10), index=list('abcdefghij'))
sb = pd.Series(np.random.random(10), index=list('abcdefghij'))
df = pd.DataFrame([sa, sb])
# 인덱스는 0, 1이고 각 칼럼이 a, b, c, d .... 인 2 x 10 테이블이 생성됨

Serise의 모든 인덱스, 즉 칼럼 이름이 일치하면 가장 깔끔한 형태의 테이블이 나오겠지만, 꼭 그렇지 않아도 상관없다. 다음과 같이 서로 다른 인덱스를 가진 데이터를 이어 붙이면, 빈 칸을 가진 테이블이 만들어진다. 빈 칸의 값은 기본적으로 NaN으로 표시된다.

pd.DataFrame([pd.Series([1,2,3,4], index=list('abcd')),
              pd.Series([5,6,7,8], index=list('cdef'))])
     a    b    c    d    e    f
0  1.0  2.0  3.0  4.0  NaN  NaN
1  NaN  NaN  5.0  6.0  7.0  8.0

크기가 균일한 2차원 리스트나 2차원 np 배열을 그대로 쓸 수 있다. 참고로 헷갈리면 안되는 것이 m * n 배열이라고 하면 각각 행, 열의 크기를 말한다. 만약 10 x 4 크기 프레임이라 하면 10개의 행이 있고, 각각의 행이 4개의 열로 구성되는 것을 말한다.

# 10 * 4의 난수 2차원 배열
np.random.rand(10, 4)
# 이를 사용한 데이터프레임 생성
dx = pd.DataFrame(np.random.rand(10, 4), columns=list('ABCD'))
          A         B         C         D
0  0.944171  0.335297  0.926419  0.318914
1  0.067818  0.561575  0.947829  0.015389
2  0.477922  0.878961  0.056429  0.086664
3  0.802534  0.388064  0.345444  0.767318
4  0.124099  0.965709  0.416867  0.831696
5  0.521238  0.690891  0.588054  0.400022
6  0.394584  0.208382  0.238559  0.430436
7  0.862894  0.704583  0.006374  0.152424
8  0.040867  0.694992  0.993268  0.665452
9  0.198071  0.050346  0.407076  0.947250

이 외에도 다른 생성방법으로는 pd.concat() 함수를 사용하는 방법이 있다. 이 함수는 데이터프레임과 Series를 합쳐주는 함수인데, Series + Series, DataFrame + DataFrame, DataFrame + Series 와 같이 어떤 경우에도 사용될 수 있다.

먼저 Series와 Series를 더해서 데이터 프레임을 만드는 방법을 알아보자. 두 Series 객체가 같은 크기와 같은 인덱스를 가지고 있을 때, 더하는 것을 예제를 통해 살펴보자.

# 두 개의 Series 생성
sa = pd.Series(np.random.rand(10), index=list('abcdefghij'))
sb = pd.Series(np.random.rand(10), index=list('abcdefghij'))
# 길이 방향으로 더한다.
pd.concat([sa, sb])
# 폭 방향으로 더한다.
pd.concat([sa, sb], axis=1)
          0         1
a  0.886328  0.992219
b  0.804096  0.732080
c  0.094406  0.422202
d  0.392120  0.061306
e  0.217201  0.110623
f  0.350078  0.630830
g  0.830117  0.611145
h  0.918168  0.125463
i  0.212481  0.990021
j  0.253870  0.074875

길이 방향으로 더했을 때에도 더해지는 것이 좀 재미있다. 인덱스는 고유한 값이 아니어도 되나보다. axis=1 옵션을 주면 횡방향으로 더해진다. 이 때는 각 Series의 인덱스가 데이터프레임의 인덱스가 된다. 즉 칼럼을 옆으로 붙여나가는 식으로 확장한다. 이 결과는 DataFrame이 된다.

만들어진 DataFrame의 각 칼럼은 0, 1, … 의 정수가 된다. 만약 각 Series 객체가 name 속성을 가지고 있다면 해당 열의 이름이 칼럼 이름이 된다. 이미 존재하는 데이터 프레임에 Series를 칼럼으로 추가하려면, Series의 인덱스와 데이터 프레임의 인덱스가 같아야 한다. (물론 달라도 행이 늘어나면서 빈 칸이 생기는 식으로 결합은 된다.)

데이터 조회

데이터프레임에서는 기본적으로 df['col']이나 df.col의 형식으로 특정한 열을 조회할 수 있다. 그외에는 loc, at과 같은 조금 특별한 인덱서(indexer)를 사용한다. loc은 특정한 행이나 열 혹은 그 조합을, at은 비슷한 문법으로 단일 값을 조회하는데 사용될 수 있다. 참고로 정수를 사용한 인덱스 각각 iloc, iat을 사용한다.

이를 위해서 약간 많은 데이터를 생성해보자. numpy.random.randn은 각 차원의 크기를 인자로 받아서 다차원배열에 표준 정규 분포 난수를 생성한다. 2차원 난수배열에 각 열에 이름을 주고, 행 인덱스로는 날짜 범위를 사용해서 표를 생성한다.

> df = pd.DataFrame(
         np.random.randn(100, 4),
         columns=list('ABCD'),
         index=pd.date_range('2020-04-01', freq='D', periods=100))

이 데이터에 대해서 특정 데이터를 조회하는 방법을 살펴보자. 먼저 iloc[]을 사용해서 특정한 행이나 특정 행의 범위를 볼 수 있다.

> # 세 번째 행을 찾는다.
> df.iloc[2]
A    0.086284
B   -0.718937
C    0.803418
D    0.441847
Name: 2020-04-02 00:00:00, dtype: float64

df.iloc[2]는 세 번째 행을 Series타입으로 리턴한다. 만약 정수리스트의 형태로 인자를 넘겨주면 데이터프레임으로 반환한다.

> df.iloc[[2]]
                   A         B         C         D
2020-04-03 -0.620411 -0.374374 -1.470118  0.363311

[2]를 받는다는 것은 [2, 7, 9]를 받을 수 있다는 의미이다. 정수의 리스트를 인덱스로 사용해서 불연속적인 여러 개의 행을 조회할 수 있다.

> df.iloc[[2, 7, 9]]
                   A         B         C         D
2020-04-03 -0.620411 -0.374374 -1.470118  0.363311
2020-04-08 -0.194046 -1.587785 -0.938767  0.283921
2020-04-10 -1.217751  0.595970  0.618266 -0.664499

리스트 대신에 2:9와 같은 슬라이스 범위를 받아, 특정한 구간 내의 연속적인 행들을 조회할 수 있다.

> df.iloc[2:9]
                   A         B         C         D
2020-04-03 -0.620411 -0.374374 -1.470118  0.363311
2020-04-04 -2.879266 -1.589720  1.101134 -0.154654
2020-04-05 -0.769641 -0.003583 -1.657023 -0.557924
2020-04-06  0.943040  0.233972  1.060563 -0.103555
2020-04-07  0.126549 -1.106255 -0.284924  2.115625
2020-04-08 -0.194046 -1.587785 -0.938767  0.283921
2020-04-09  0.781117  0.404263 -0.929021  0.299767

행범위와 열범위를 콤마로 구분하여 보다 작은 그리드를 만들 수 있다.

> df.iloc[2:9, [1,3]]
                   B         D
2020-04-03 -0.374374  0.363311
2020-04-04 -1.589720 -0.154654
2020-04-05 -0.003583 -0.557924
2020-04-06  0.233972 -0.103555
2020-04-07 -1.106255  2.115625
2020-04-08 -1.587785  0.283921
2020-04-09  0.404263  0.299767

iloc 대신에 loc을 쓰면 정수 대신 다른 조건을 사용할 수 있다. 여기에는…

  • 칼럼이름이나 인덱스값 (정수 아닌)
  • 칼럼이름, 인덱스의 범위 – 'A':'C' 와 같은 식으로 쓸 수 있다. 이는 정수 범위 슬라이스와 달리 끝값이 포함된다.
  • 칼럼이름, 인덱스 이름의 리스트
  • List(bool) 타입의 리스트 – 값이 True인 행이나 열이 포함된다.
  • 위의 조건을 사용해서 만든 [행범위, 열범위] 로 표의 일부만 조회할 수 있다.

예를 들어 B 값이 양수인 행에 대해서 A, C, D 열의 값들을 조회하고자 한다면? df.BSeries를 얻을 수 있고 여기에 연산을 통해서 불리언 값의 배열을 얻게된다는 점을 이용하면 다음의 문법을 사용할 수 있다.

> df.loc[df.B >= 0, ['A', 'C', 'D']]
                   A         C         D
2020-04-01 -1.199800 -1.026626 -0.135774
2020-04-06  0.943040  1.060563 -0.103555
2020-04-09  0.781117 -0.929021  0.299767
2020-04-10 -1.217751  0.618266 -0.664499

데이터 살펴보기

표형식의 레이아웃을 확인하기 위해서 REPL 상에서 데이터프레임 전체를 찍어보는 것은 제법 불편하다. 데이터의 모양을 살펴볼 수 있는 몇 가지 메소드가 있다. 데이터프레임이나 시리즈 객체는 내부 데이터의 개괄적인 모습을 보여주는 API를 제공한다. 예를 들어 .max()는 최대값을, .sum()은 합계를 보여주는 식이다.

  • shape : 각 차원의 크기를 튜플로 보여준다.
  • size : 데이터의 전체 크기
  • head() , tail() : 데이터의 앞, 뒤에서 5행씩을 보여준다. 보여줄 크기를 넘겨서 더 많이 혹은 더 적게 볼 수 있다.
  • columns, index : 각 컬럼과 인덱스를 보여준다. axes는 모든 축에 대한 정보를 보여준다.
  • values : 전체 값을 N차원 ndarray로 보여준다.

https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.html

흥미로운 메소드로 describe()가 있다. 이는 데이터들의 기본적인 통계량을 요약해서 일목요연하게 표시해준다. 각각의 통계량에 대해서는 sum(), max(), mean(), std() 등의 메소드를 통해서 구할 수 있다.

pandas의 분산과 표준편차에 대해서 알아둘 점. pandas.Series나 pandas.DataFrame에서 std,

# `df.ndim`의 값은 2이다. 따라서 축은 0번, 1번이 있다고 가정한다.
# 인자가 없는 통계량 함수는 기본적으로 0번 축을 기준으로 계산한다.
> df.sum()
A    2.150382
B   -1.238761
C    0.940304
D    1.482810
dtype: float64
# 세로축 방향으로 계산할 수 있다. (0->X축, 1->Y축)
> df.sum(1)
2020-04-01   -1.958910
2020-04-02   -0.033413
2020-04-03   -1.841984
2020-04-04    2.006316
2020-04-05    0.441786
2020-04-06   -1.083911
2020-04-07   -0.485753
2020-04-08    3.663681
2020-04-09    1.310737
2020-04-10    1.316187
Freq: D, dtype: float64

데이터 변경

특정한 범위, 열, 행 혹은 단일 데이터를 특정했다면 해당 범위에 값을 대입하여 기존 데이터를 변경할 수 있다. 데이터의 일부분을 인덱서를 사용해서 특정했을 때 리턴되는 값들은 원본 데이터의 개별 데이터를 참조하기 때문에 이러한 변형들은 원본 데이터에 영향을 준다.

데이터를 변경할 때 값은 지정한 범위의 형태와 같은 데이터프레임 혹은 Series 객체가 된다. 단일 값을 지정할 수도 있는데, 이 경우에는 지정된 범위가 모두 같은 값으로 변경된다.

> s = pd.Series(np.random.randn(5))
> s
0    1.273054
1   -1.024466
2    0.156936
3    0.074036
4   -0.828118
dtype: float64
# 2, 3번 요소를 변경한다.
> s[2:4] = [1, 5]
0    1.273054
1   -1.024466
2    1.000000  <
3    5.000000  <
4   -0.828118
dtype: float64
# 전체를 바꾸기
> s[:] = [5, 3, 2, 1, 9]
0    1
1    2
2    3
3    4
4    5
dtype: int32  --> 타입이 바뀌었다.
> s[:] = np.array([5,3,2,1,9], dtype=np.float64)
0    5.0
1    3.0
2    2.0
3    1.0
4    9.0
dtype: float64  

위 예에서 보듯이 변경할 범위와 차원이 같은 nparray나 파이썬 리스트를 통해서 Series의 전체 및 일부를 교체하는 것이 가능함을 볼 수 있다. 앞서 언급한 바와 같이 단일 값을 지정하여 해당 범위를 모두 같은 값으로 변경하는 것도 가능하다.

> s[:] = 1.0
0    1.0
1    1.0
2    1.0
3    1.0
4    1.0
dtype: float64

데이터 프레임에 대해서도 동일한 변경이 가능하다. 단일 값을 대입하여 모든 데이터 값을 같은 값으로 바꾸거나, 특정한 행/열을 다른 값으로 바꿀 수 있다.

# 2~6행의 값을 모두 2, 3, 4, 5로 바꾼다.
> df.iloc[2:6, :] = [2, 3, 4, 5]
# 1~5 행의 모든 열의 값을 5.4로 바꾼다.
> df.iloc[0:6] = 5.4

데이터 프레임의 2차원 부분 집합 역시 데이터 프레임인데, 이를 다른 데이터 프레임으로 변경하는 것은 예상대로 작동하지 않는다. 데이터프레임은 각 행방향으로 증가하는 인덱스를 그 내부에 가지고 있는데, 대입해주려는 새로운 데이터를 포함한 데이터 프레임의 각 행의 인덱스가 원본의 그것과 같지 않기 때문이다. 이 경우는 대입해주려는 데이터를 다차원 np 배열로 변환해주면 된다.

df = pd.DataFrame({'a': range(1, 6), 'b': [1, 11, 111, 1111, 11111]})
d1 = pd.DataFrame(np.random.randn(3, 2), columns=list('ab'))
df.loc[2:4] = d1
----------------------
          a          b
0  1.000000   1.000000
1  2.000000  11.000000
2  0.118021   0.775048
3       NaN        NaN
4       NaN        NaN
----------------------
# 이렇게 해야 한다.
df.loc[2:4] = d1.to_numpy()
----------------------
          a          b
0  1.000000   1.000000
1  2.000000  11.000000
2  1.235126   0.253898
3  0.574356  -0.631452
4  0.118021   0.775048
----------------------

이상으로 pandas의 기본 요소인 Series와 데이터 프레임을 생성하고 사용하는 가장 기본적인 방법을 살펴보았다. 데이터 프레임은 단순히 Series끼리 결합된 정보의 덩어리를 넘어서서 데이터들의 관계에 따라서 더 많은 정보를 획득할 수 있는 수단이 되기도 한다. 다음 글에서는 데이터 프레임에서의 피봇팅 및 그외 다른 통계 기능을 어떻게 사용하는지 살펴보기로 하겠다.

댓글 남기기