# Pandas 자료구조

## Pandas 자료구조

데이터의 형태나 속성은 매우 다양하다. 이때 서로 다른 형식을 갖는 데이터를 컴퓨터가 이해할 수 있도록 동일한 형식을 갖는 구조로 통합할 필요가 있다. 이때 pandas의 Series와 DataFrame을 사용하여 공통의 포맷으로 정리할 수 있다.

### Series

순차적으로 나열된 1차원 배열의 형태를 갖는다. python dictionary와 비슷한 구조이기 때문에 dictionary를 series로 변환하는 방법을 많이 사용한다.

```python
pandas.Series(dictionary)
```

```python
# dictionary
dict_data = {'a': 11, 'b': 2, 'c': 3}

# dictionary to Series
sr = pd.Series(dict_data)

print(type(sr))
print(sr)
```

```
<class 'pandas.core.series.Series'>


a    11
b     2
c     3
dtype: int64
```

index 속성을 이용해 인덱스 배열(`.index`)을 따로 선택할 수 있으며, 데이터 값(`.values`) 배열도 따로 가져올 수 있다.

```python
print(sr.index) # Index(['a', 'b', 'c'], dtype='object')
print(sr.values)
```

만약 인덱스를 따로 지정하지 않는다면, default로 정수형 위치 인덱스가 자동으로 지정된다.

```python
>>> list_data = ['20190428', 3.14, 'ABC', 100, True]
>>> sr = pd.Series(list_data)
>>> print(sr)
0    20190428
1        3.14
2         ABC
3         100
4        True
dtype: object
```

```python
>>> print(sr.index)
RangeIndex(start=0, stop=5, step=1)
```

0에서 4까지 RangeIndex가 생성된 것을 확인할 수 있다.

이때 특정 범위의 Series Data를 가져올 수도 있다. `[]` 안에 가져오고 싶은 특정 index위치를 입력하면된다. 만약 정수형 인덱스가 아니라면 `''` 혹은 `""`로 불러 올 수 있다.

```python
>>> sr[1:4]
1    3.14
2     ABC
3     100
dtype: object
```

Default로 정수형 인덱스를 생성할 수도 있지만, 특정 인덱스를 지정해서 생성할 수도 있다.

```python
>>> tup_data= ('다혜', '2020', 'W', True)
>>> sr = pd.Series(tup_data, index=['이름', '연도', '성별', '직장여부'])
>>> print(sr)
이름        다혜
연도      2020
성별         W
직장여부    True
dtype: object
```

다음과 같이 `index=[]` 로 특정 인덱스를 리스트 형태로 전달해 생성할 수 있다. 만약 다수의 데이터를 가져오고 싶은 경우에는 다음과 같이 불러올 수 있다.

```python
>>> sr[[1,3]]
연도      2020
직장여부    True
dtype: object
```

### DataFrame

DataFrame은 2차원 배열 구조로, R의 Dataframe에서 유래됐다. Dataframe의 각 열은 각각의 Series 객체이다. Series를 열벡터(vector)라고 하면, DataFrame은 여러개의 열벡터들이 같은 행 인덱스를 기준으로 줄지어 결합된 2차원 벡터 혹은 행렬(matrix)이다.

DataFrame의 열은 공통의 속성을 갖는 일련의 데이터를 나타내교, 행은 개별 관측대상에 대한 다양한 속성 데이터들의 모음인 레코드(record)가 된다.

```python
pandas.DataFrame(dictionary)
```

다음과 같이 DataFrame을 생성할 수 있다.

```python
>>> dict_data = {'c0': [1,2,3], 'c1':[4,5,6], 'c2': [7,8,9]}
>>> df = pd.DataFrame(dict_data)
>>> print(type(df))
<class 'pandas.core.frame.DataFrame'>
>>> print(df)
   c0  c1  c2
0   1   4   7
1   2   5   8
2   3   6   9
```

#### 행 인덱스, 열 이름

2차원 배열을 DataFrame() 함수 인자로 전달해 DataFrame으로 변환할 때 행 인덱스와 열 이름 속성을 사용자가 직접 지정할 수도 있다.

```python
pandas.DataFrame(2차원배열, index=행 인덱스 배열, columns=열 이름 배열)
```

```python
>>> df = pd.DataFrame([[15, '남', '덕영중'], [17, '여', '서원고']], index=['준서', '예은'], columns=['나이', '성별', '학교'])
>>> print(df)
    나이 성별   학교
준서  15  남  덕영중
예은  17  여  서원고
```

행 인덱스와 열 이름을 변경할 수도 있다.

```python
df.index = 새로운 행 인덱스 배열
df.values = 새로운 열 이름 배열
```

```python
df.index = ['학생1', '학생2']
>>> df.columns=['연령','남녀','소속']
>>> print(df)
     연령 남녀   소속
학생1  15  남  덕영중
학생2  17  여  서원고
```

특정 인덱스와 열만 변경하고 싶은 경우 `rename()` 메소드를 사용하면된다.

```python
df.rename(index={기존인덱스:새인덱스, ...})
df.rename(columns={기존이름:새이름, ...})
```

이때 원본 객체를 직접 수정하는 것이 아니라 새로운 DataFrame 객체를 반환한다. 만약 원본 객체를 변경하려면 `inplace=True` 옵션을 지정하면된다.

```python
>>> df.rename(index={'학생1':'S1', '학생2':'S2'}, inplace=True)
>>> print(df)
    연령 남녀   소속
S1  15  남  덕영중
S2  17  여  서원고
```

#### 행/열 삭제

```python
df.drop(행 인덱스 or 배열, axis=0) # axis=0 default
df.drop(열 이름 or 배열, axis=1)
```

`drop()` 메소드를 이용해 행 혹은 열을 삭제할 수 있다. 이때, 원복 객체를 변경하지 않고 새로운 객체를 반환한다. 만약 원본 객체를 직접 변경하고 싶은 경우 `inplace=True` 옵션을 주면된다.

```python
>>> df2 = df.drop('소속', axis=1)
>>> print(df2)
    연령 남녀
S1  15  남
S2  17  여
>>> df2.drop('S1', inplace=True)
>>> print(df2)
    연령 남녀
S2  17  여
```

#### 행/열/원소 선택

* 행 선택

| 구분    | loc                                  | iloc                                        |
| ----- | ------------------------------------ | ------------------------------------------- |
| 탐색 대상 | 인덱스 이름(index label)                  | 정수형 위치 인덱스(interger position)               |
| 범위 지정 | <p>가능(범위의 끝 포함)<br>ex) ['a':'c']</p> | <p>가능(범위의 끝 제외)<br>ex) [3:7] -> 3,4,5,6</p> |

```python
>>> df.loc['상혁']
수학     90
영어     98
사회     70
과학    100
Name: 상혁, dtype: int64
>>> df.iloc[0]
수학     90
영어     98
사회     70
과학    100
Name: 상혁, dtype: int64
    
>>> df.loc[['상혁','우찬']]
    수학  영어  사회   과학
상혁  90  98  70  100
우찬  70  95  80   90

>>> df.iloc[0:1]
    수학  영어  사회   과학
상혁  90  98  70  100
```

범위지정으로 데이터를 가져오는 경우 슬라이싱 간격을 지정해서 가져올 수 있다.

```python
>>> df.iloc[::2]
    수학  영어  사회   과학
상혁  90  98  70  100
우찬  70  95  80   90
```

역순으로 인덱싱 하려면 `[::-1]` 을 하면된다.

```python
>>> df.iloc[::-1]
    수학  영어  사회   과학
우찬  70  95  80   90
상호  80  89  85   90
상혁  90  98  70  100
```

* 열 선택

```python
df["열 이름"]
df.열이름
```

단일 열을 가져올 때는 다음과 같이 가져올 수 있다. 두 번째 방법(`.`)은 열 이름이 문자열인 경우에만 가능하다. 단일 열을 가져오는 경우 Series 객체를 반환한다.

```python
df[[열1,열2, ..., 열n]]
```

`[[]]` 를 사용해 열을 여러개 선택할 수 있다. `[[]]` 로 여러개의 열을 가져오는 경우 DataFrame 객체를 반환하며, `[[열1]]` 로 가져오는 경우에도 DataFrame 객체를 반환한다.

```python
>>> df.수학
상혁    90
상호    80
우찬    70
Name: 수학, dtype: int64
>>> df['영어']
상혁    98
상호    89
우찬    95
Name: 영어, dtype: int64
>>> df[['영어','수학']]
    영어  수학
상혁  98  90
상호  89  80
우찬  95  70
```

* 원소 선택

```python
df.loc[행인덱스, 열 이름]
df.iloc[행번호, 열번호]
```

```python
>>> df.loc['상혁', '수학']
90
>>> df.loc['상혁', ['수학', '과학']]
수학     90
과학    100
Name: 상혁, dtype: int64
>>> df.iloc[1,[1,3]]
영어    89
과학    90
Name: 상호, dtype: int64
>>> df.iloc[1,1:]
영어    89
사회    85
과학    90
Name: 상호, dtype: int64
>>> df.iloc[0:2,1:]
    영어  사회   과학
상혁  98  70  100
상호  89  85   90
>>> df.iloc[0:2,1:3]
    영어  사회
상혁  98  70
상호  89  85
```

#### 값 추가

* 열 추가

```python
df['새로운 열 이름'] = 데이터 값
```

```python
>>> df['국어'] = [100,80,90,70,80]
>>> print(df)
     수학  영어  사회   과학   국어
상혁  100  98  70  100  100
상호   80  89  85   90   80
우찬   70  95  80   90   90
진성   93  93  80   86   70
창동   70  80  90  100   80
```

* 행추가

```python
df.loc['새로운 행 이름'] = 데이터 값 or 배열
```

하나의 데이터 값을 입력하게 되면 행의 모든 원소에 동일한 값이 추가되며, 후자의 경우 배열의 순서대로 열 위치에 값이 하나씩 추가된다.

```python
>>> df.loc['진성'] = 0
>>> print(df)
    수학  영어  사회   과학
상혁  90  98  70  100
상호  80  89  85   90
우찬  70  95  80   90
진성   0   0   0    0
>>> df.loc['창동'] = [70,80,90,100]
>>> print(df)
    수학  영어  사회   과학
상혁  90  98  70  100
상호  80  89  85   90
우찬  70  95  80   90
진성   0   0   0    0
창동  70  80  90  100
```

* 원소 값 변경

```python
>>> df.iloc[0][0]=100
>>> print(df)
     수학  영어  사회   과학
상혁  100  98  70  100
상호   80  89  85   90
우찬   70  95  80   90
진성    0   0   0    0
창동   70  80  90  100
>>> df.loc['진성',['수학','영어']] = 93
>>> print(df)
     수학  영어  사회   과학
상혁  100  98  70  100
상호   80  89  85   90
우찬   70  95  80   90
진성   93  93   0    0
창동   70  80  90  100
>>> df.loc['진성',['사회','과학']] = 80,86
>>> print(df)
     수학  영어  사회   과학
상혁  100  98  70  100
상호   80  89  85   90
우찬   70  95  80   90
진성   93  93  80   86
창동   70  80  90  100
```

#### 행 열 위치 변경

```python
df.transpose()
df.T
```

```python
>>> print(df)
     수학  영어  사회   과학   국어
상혁  100  98  70  100  100
상호   80  89  85   90   80
우찬   70  95  80   90   90
진성   93  93  80   86   70
창동   70  80  90  100   80
>>> print(df.T)
     상혁  상호  우찬  진성   창동
수학  100  80  70  93   70
영어   98  89  95  93   80
사회   70  85  80  80   90
과학  100  90  90  86  100
국어  100  80  90  70   80
```

## Index

* 특정 열을 행 인덱스로 설정

```python
df.set_index(['열 이름'])
df.set_index('열 이름')
```

특정 열을 행 인덱스로 사용할 수 있다. 이때 원본 객체를 변경하지 않고 새로운 객체를 반환한다. 만약 원본 객체에 할당하고 싶은 경우 `inplace=True` 옵션을 주면된다.

이때 행 인덱스를 여러개를 줄 수도 있으며, 이런 경우 MulitiIndex라고 하며, set\_index() 로 새로 인덱스를 지정하면, 기존 행 인덱스는 삭제된다.

```python
>>> df.set_index('수학')
     영어  사회   과학   국어
수학
100  98  70  100  100
80   89  85   90   80
70   95  80   90   90
93   93  80   86   70
70   80  90  100   80
>>> df.set_index(['수학','영어'])
        사회   과학   국어
수학  영어
100 98  70  100  100
80  89  85   90   80
70  95  80   90   90
93  93  80   86   70
70  80  90  100   80
```

* 행 인덱스 재배열

```python
df.reindex(새로운 인덱스 배열, fill_value=값)
```

기존 데이터프레임에 존재하지 않는 행 인덱스가 새롭게 추가되는 경우 NaN(Not a Number) 값이 입력된다. NaN대신 유효한 값을 채우려면 `fill_value` 옵션에 원하는 값을 입력하면된다.

```python
>>> new_index = ['상혁','상호','우찬','진성','창동', 'NN']
>>> df.reindex(new_index)
       수학    영어    사회     과학     국어
상혁  100.0  98.0  70.0  100.0  100.0
상호   80.0  89.0  85.0   90.0   80.0
우찬   70.0  95.0  80.0   90.0   90.0
진성   93.0  93.0  80.0   86.0   70.0
창동   70.0  80.0  90.0  100.0   80.0
NN    NaN   NaN   NaN    NaN    NaN
>>> df.reindex(new_index, fill_value=0)
     수학  영어  사회   과학   국어
상혁  100  98  70  100  100
상호   80  89  85   90   80
우찬   70  95  80   90   90
진성   93  93  80   86   70
창동   70  80  90  100   80
NN    0   0   0    0    0
```

* 행 인덱스 초기화

```python
df.reset_index()
```

행 인덱스를 정수형 위치 인덱스로 초기화할 수 있다. 이때 기존 행 인덱스는 열로 이동한다.

```python
>>> df.reset_index()
  index   수학  영어  사회   과학   국어
0    상혁  100  98  70  100  100
1    상호   80  89  85   90   80
2    우찬   70  95  80   90   90
3    진성   93  93  80   86   70
4    창동   70  80  90  100   80
```

* 행 인덱스 기준으로 데이터 프레임 정렬

```python
df.sort_index(ascending=True/False) # 오름차순/내림차순
```

ascending 옵션을 사용해 오름차순, 내림차순 정렬을 할 수 있다.

```python
>>> df.sort_index()
     수학  영어  사회   과학   국어
상혁  100  98  70  100  100
상호   80  89  85   90   80
우찬   70  95  80   90   90
진성   93  93  80   86   70
창동   70  80  90  100   80
>>> df.sort_index(ascending=False)
     수학  영어  사회   과학   국어
창동   70  80  90  100   80
진성   93  93  80   86   70
우찬   70  95  80   90   90
상호   80  89  85   90   80
상혁  100  98  70  100  100
```

* 특정 열의 데이터 값을 기준으로 DataFrame을 정렬할 수 있다.

```python
df.sort_values()
```

ascending 옵션으로 오름차순, 내림차순 정렬을 설정할 수 있다.

```python
>>> df.sort_values(by='수학')
     수학  영어  사회   과학   국어
우찬   70  95  80   90   90
창동   70  80  90  100   80
상호   80  89  85   90   80
진성   93  93  80   86   70
상혁  100  98  70  100  100
>>> df.sort_values(by='과학', ascending=False)
     수학  영어  사회   과학   국어
상혁  100  98  70  100  100
창동   70  80  90  100   80
상호   80  89  85   90   80
우찬   70  95  80   90   90
진성   93  93  80   86   70
```

### 산술연산

Pandas 객체의 산술연산은 내부적으로 3단계 프로세스를 거친다.

```
행/열 인덱스 기준으로 모든 원소 정렬
→ 동일한 위치에 있는 원소끼리 일대일 대응
→ 일대일 대응되는 원소끼리 연산처리
```

이때 일대일 대응되는 원소가 없으면 NaN 처리한다.

#### Series 연산

* Series 숫자 연산

```python
Series + 연산자(+,-,*,/) + 숫자
```

숫자 연산을 하면 개별 원소에 각 숫자를 연산자에 맞게 계산한다.

```python
>>> sr
수학    100
영어     98
사회     70
과학    100
국어    100
Name: 상혁, dtype: int64
>>> sr2 = sr / 100
>>> sr2
수학    1.00
영어    0.98
사회    0.70
과학    1.00
국어    1.00
```

* Series + Series

```python
Series + 연산자(+,-,*,/) + Series
```

모든 인덱스에 대해 같은 인덱스를 가진 원소끼리 연산한다.

```python
>>> sr2
수학    93
영어    93
사회    80
과학    86
국어    70
Name: 진성, dtype: int64
>>> sr
수학    100
영어     98
사회     70
과학    100
국어    100
Name: 상혁, dtype: int64
>>> sr + sr2
수학    193
영어    191
사회    150
과학    186
국어    170
dtype: int64
```

이때 인덱스의 순서가 다르더라도 같은 인덱스를 찾아서 정렬한 후 계산한다. 연산하는 두 Series의 원소 개수가 다르거나, 인덱스 값이 다른 경우에는 NaN 처리를 한다.

```python
>>> sr2 = df.loc['진성']['수학':'사회']
>>> sr2
수학    93
영어    93
사회    80
Name: 진성, dtype: int64
>>> sr = df.loc['상혁']['사회':'국어']
>>> sr
사회     70
과학    100
국어    100
Name: 상혁, dtype: int64
>>> sr+sr2
과학      NaN
국어      NaN
사회    150.0
수학      NaN
영어      NaN
dtype: float64
```

이때 NaN이 포함된 값의 연산은 모두 NaN처리가 된다. 이때 NaN으로 반환되는 경우를 피하려면 fill\_value 옵션을 설정해 적용할 수 있다.

```python
sr1.add(sr2,fill_value=0) # add, sub, mul, div
```

```python
>>> sr.add(sr2, fill_value=0)
과학    100.0
국어    100.0
사회    150.0
수학     93.0
영어     93.0
dtype: float64
```

#### DataFrame 연산

* DataFrame + 숫자

```python
DataFrame + 연산자(+,-,/,*) + 숫자
```

모든 원소에 숫자를 연산자에 맞게 계산한다.

```python
>>> df = titanic.loc[:, ['age','fare']]
>>> print(df.head()) # 첫 5행만 표시
    age     fare
0  22.0   7.2500
1  38.0  71.2833
2  26.0   7.9250
3  35.0  53.1000
4  35.0   8.0500
>>> df2 =df + 10
>>> df2.head()
    age     fare
0  32.0  17.2500
1  48.0  81.2833
2  36.0  17.9250
3  45.0  63.1000
4  45.0  18.0500
```

* DataFrame + DataFrame

```python
DataFrame + 연산자(+,-,/,*) + DataFrame
```

각 데이터 프레임의 같은 행, 같은 열 위치에 있는 원소끼리 계산하며, 어느 한쪽 원소가 존재하지 않거나 NaN이면 연산 결과는 NaN으로 처리한다.

```python
>>> df2 =df + 10
>>> result=df2-df
>>> result.tail() # 마지막 5행
      age  fare
886  10.0  10.0
887  10.0  10.0
888   NaN  10.0
889  10.0  10.0
890  10.0  10.0
```

### 참고

* [파이썬 머신러닝 판다스 데이터 분석](http://digital.kyobobook.co.kr/digital/ebook/ebookDetail.ink?LINK=NVE\&category=001\&barcode=4808956748337)
