4.4 데이터프레임의 데이터 조작

판다스는 넘파이 2차원 배열에서 가능한 대부분의 데이터 처리가 가능하며 추가로 데이터 처리 및 변환을 위한 다양한 함수와 메서드를 제공한다.

데이터 갯수 세기

가장 간단한 데이터 분석은 데이터의 갯수를 세는 것이다. count 메서드를 사용한다. NaN 값은 세지 않는다.

s = pd.Series(range(10))
s[3] = np.nan
s
0    0.0
1    1.0
2    2.0
3    NaN
4    4.0
5    5.0
6    6.0
7    7.0
8    8.0
9    9.0
dtype: float64
s.count()
9

데이터프레임에서는 각 열마다 별도로 데이터 갯수를 센다. 데이터에서 값이 누락된 부분을 찾을 때 유용하다.

np.random.seed(2)
df = pd.DataFrame(np.random.randint(5, size=(4, 4)), dtype=float)
df.iloc[2, 3] = np.nan
df
0 1 2 3
0 0.0 0.0 3.0 2.0
1 3.0 0.0 2.0 1.0
2 3.0 2.0 4.0 NaN
3 4.0 3.0 4.0 2.0
df.count()
0    4
1    4
2    4
3    3
dtype: int64

다음 명령으로 타이타닉호의 승객 데이터를 데이터프레임으로 읽어올 수 있다. 이 명령을 실행하려면 seaborn 패키지가 설치되어 있어야 한다.

import seaborn as sns
titanic = sns.load_dataset("titanic")
titanic.head()
survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone
0 0 3 male 22.0 1 0 7.2500 S Third man True NaN Southampton no False
1 1 1 female 38.0 1 0 71.2833 C First woman False C Cherbourg yes False
2 1 3 female 26.0 0 0 7.9250 S Third woman False NaN Southampton yes True
3 1 1 female 35.0 1 0 53.1000 S First woman False C Southampton yes False
4 0 3 male 35.0 0 0 8.0500 S Third man True NaN Southampton no True

연습 문제 4.4.1

타이타닉호 승객 데이터의 데이터 개수를 각 열마다 구해본다.

카테고리 값 세기

시리즈의 값이 정수, 문자열, 카테고리 값인 경우에는 value_counts 메서드로 각각의 값이 나온 횟수를 셀 수 있다.

np.random.seed(1)
s2 = pd.Series(np.random.randint(6, size=100))
s2.tail()
95    4
96    5
97    2
98    4
99    3
dtype: int64
s2.value_counts()
1    22
0    18
4    17
5    16
3    14
2    13
dtype: int64

데이터프레임에는 value_counts 메서드가 없으므로 각 열마다 별도로 적용해야 한다.

df[0].value_counts()
3.0    2
4.0    1
0.0    1
Name: 0, dtype: int64

정렬

데이터를 정렬하려면 sort_index 메서드 sort_values 메서드를 사용한다. sort_index 메서드는 인덱스 값을 기준으로, sort_values 메서드는 데이터 값을 기준으로 정렬한다.

앞에서 s2 시리즈의 각 데이터 값에 따른 데이터 갯수를 인덱스에 따라 정렬하려면 다음처럼 sort_index를 적용한다.

s2.value_counts().sort_index()
0    18
1    22
2    13
3    14
4    17
5    16
dtype: int64

NaN값이 있는 경우에는 정렬하면 NaN값이 가장 나중으로 간다.

s.sort_values()
0    0.0
1    1.0
2    2.0
4    4.0
5    5.0
6    6.0
7    7.0
8    8.0
9    9.0
3    NaN
dtype: float64

큰 수에서 작은 수로 반대 방향 정렬하려면 ascending=False 인수를 지정한다.

s.sort_values(ascending=False)
9    9.0
8    8.0
7    7.0
6    6.0
5    5.0
4    4.0
2    2.0
1    1.0
0    0.0
3    NaN
dtype: float64

데이터프레임에서 sort_values 메서드를 사용하려면 by 인수로 정렬 기준이 되는 열을 지정해 주어야 한다.

df.sort_values(by=1)
0 1 2 3
0 0.0 0.0 3.0 2.0
1 3.0 0.0 2.0 1.0
2 3.0 2.0 4.0 NaN
3 4.0 3.0 4.0 2.0

by 인수에 리스트 값을 넣으면 이 순서대로 정렬 기준의 우선 순위가 된다. 즉, 리스트의 첫번째 열을 기준으로 정렬한 후 동일한 값이 나오면 그 다음 열로 순서를 따지게 된다.

df.sort_values(by=[1, 2])
0 1 2 3
1 3.0 0.0 2.0 1.0
0 0.0 0.0 3.0 2.0
2 3.0 2.0 4.0 NaN
3 4.0 3.0 4.0 2.0

연습 문제 4.4.2

sort_values 메서드를 사용하여 타이타닉호 승객에 대해 성별(sex) 인원수, 나이별(age) 인원수, 선실별(class) 인원수, 사망/생존(alive) 인원수를 구하라.

행/열 합계

행과 열의 합계를 구할 때는 sum(axis) 메서드를 사용한다. axis 인수에는 합계로 인해 없어지는 방향축(0=행, 1=열)을 지정한다.

np.random.seed(1)
df2 = pd.DataFrame(np.random.randint(10, size=(4, 8)))
df2
0 1 2 3 4 5 6 7
0 5 8 9 5 0 0 1 7
1 6 9 2 4 5 2 4 2
2 4 7 7 9 1 7 0 6
3 9 9 7 6 9 1 0 1

행방향 합계를 구할 때는 sum(axis=1) 메서드를 사용한다.

df2.sum(axis=1)
0    35
1    34
2    41
3    42
dtype: int64
df2["RowSum"] = df2.sum(axis=1)
df2
0 1 2 3 4 5 6 7 RowSum
0 5 8 9 5 0 0 1 7 35
1 6 9 2 4 5 2 4 2 34
2 4 7 7 9 1 7 0 6 41
3 9 9 7 6 9 1 0 1 42

열 합계를 구할 때는 sum(axis=0) 메서드를 사용하는데 axis인수의 디폴트 값이 0이므로 axis인수를 생략할 수 있다.

df2.sum()
0          24
1          33
2          25
3          24
4          15
5          10
6           5
7          16
RowSum    152
dtype: int64
df2.loc["ColTotal", :] = df2.sum()
df2
0 1 2 3 4 5 6 7 RowSum
0 5.0 8.0 9.0 5.0 0.0 0.0 1.0 7.0 35.0
1 6.0 9.0 2.0 4.0 5.0 2.0 4.0 2.0 34.0
2 4.0 7.0 7.0 9.0 1.0 7.0 0.0 6.0 41.0
3 9.0 9.0 7.0 6.0 9.0 1.0 0.0 1.0 42.0
ColTotal 24.0 33.0 25.0 24.0 15.0 10.0 5.0 16.0 152.0

mean 메서드는 평균을 구하며 sum 메서드와 사용법이 같다.

연습 문제 4.4.3

  1. 타이타닉호 승객의 평균 나이를 구하라.

  2. 타이타닉호 승객중 여성 승객의 평균 나이를 구하라.

  3. 타이타닉호 승객중 1등실 선실의 여성 승객의 평균 나이를 구하라.

apply 변환

행이나 열 단위로 더 복잡한 처리를 하고 싶을 때는 apply 메서드를 사용한다. 인수로 행 또는 열을 받는 함수를 apply 메서드의 인수로 넣으면 각 열(또는 행)을 반복하여 그 함수에 적용시킨다.

df3 = pd.DataFrame({
    'A': [1, 3, 4, 3, 4],
    'B': [2, 3, 1, 2, 3],
    'C': [1, 5, 2, 4, 4]
})
df3
A B C
0 1 2 1
1 3 3 5
2 4 1 2
3 3 2 4
4 4 3 4

예를 들어 각 열의 최대값과 최소값의 차이를 구하고 싶으면 다음과 같은 람다 함수를 넣는다.

df3.apply(lambda x: x.max() - x.min())
A    3
B    2
C    4
dtype: int64

만약 행에 대해 적용하고 싶으면 axis=1 인수를 쓴다.

df3.apply(lambda x: x.max() - x.min(), axis=1)
0    1
1    2
2    3
3    2
4    1
dtype: int64

각 열에 대해 어떤 값이 얼마나 사용되었는지 알고 싶다면 value_counts 함수를 넣으면 된다.

df3.apply(pd.value_counts)
A B C
1 1.0 1.0 1.0
2 NaN 2.0 1.0
3 2.0 2.0 NaN
4 2.0 NaN 2.0
5 NaN NaN 1.0

다음과 같이 타이타닉호의 승객 중 나이 20살을 기준으로 성인(adult)과 미성년자(child)를 구별하는 라벨 열을 만들 수 있다.

titanic["adult/child"] = titanic.apply(lambda r: "adult" if r.age >= 20 else "child", axis=1)
titanic.tail()
survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone adult/child
886 0 2 male 27.0 0 0 13.00 S Second man True NaN Southampton no True adult
887 1 1 female 19.0 0 0 30.00 S First woman False B Southampton yes True child
888 0 3 female NaN 1 2 23.45 S Third woman False NaN Southampton no False child
889 1 1 male 26.0 0 0 30.00 C First man True C Cherbourg yes True adult
890 0 3 male 32.0 0 0 7.75 Q Third man True NaN Queenstown no True adult

연습 문제 4.4.4

타이타닉호의 승객에 대해 나이와 성별에 의한 카테고리 열인 category1 열을 만들어라. category1 카테고리는 다음과 같이 정의된다.

  1. 20살이 넘으면 성별을 그대로 사용한다.

  2. 20살 미만이면 성별에 관계없이 “child”라고 한다.

fillna 메서드

NaN 값은 fillna 메서드를 사용하여 원하는 값으로 바꿀 수 있다.

df3.apply(pd.value_counts).fillna(0.0)
A B C
1 1.0 1.0 1.0
2 0.0 2.0 1.0
3 2.0 2.0 0.0
4 2.0 0.0 2.0
5 0.0 0.0 1.0

연습 문제 4.4.5

타이타닉호의 승객 중 나이를 명시하지 않은 고객은 나이를 명시한 고객의 평균 나이 값이 되도록 titanic 데이터프레임을 고쳐라.

astype 메서드

astype 메서드로 전체 데이터의 자료형을 바꾸는 것도 가능하다.

df3.apply(pd.value_counts).fillna(0).astype(int)
A B C
1 1 1 1
2 0 2 1
3 2 2 0
4 2 0 2
5 0 0 1

연습 문제 4.4.6

타이타닉호의 승객에 대해 나이와 성별에 의한 카테고리 열인 category2 열을 만들어라. category2 카테고리는 다음과 같이 정의된다.

  1. 성별을 나타내는 문자열 male 또는 female로 시작한다.

  2. 성별을 나타내는 문자열 뒤에 나이를 나타내는 문자열이 온다.

  3. 예를 들어 27살 남성은 male27 값이 된다.

실수 값을 카테고리 값으로 변환

실수 값을 크기 기준으로 하여 카테고리 값으로 변환하고 싶을 때는 다음과 같은 명령을 사용한다.

  • cut: 실수 값의 경계선을 지정하는 경우

  • qcut: 갯수가 똑같은 구간으로 나누는 경우

예를 들어 다음과 같은 나이 데이터가 있다고 하자.

ages = [0, 2, 10, 21, 23, 37, 31, 61, 20, 41, 32, 101]

cut 명령을 사용하면 실수값을 다음처럼 카테고리 값으로 바꿀 수 있다. bins 인수는 카테고리를 나누는 기준값이 된다. 영역을 넘는 값은 NaN으로 처리된다.

bins = [1, 20, 30, 50, 70, 100]
labels = ["미성년자", "청년", "중년", "장년", "노년"]
cats = pd.cut(ages, bins, labels=labels)
cats
[NaN, 미성년자, 미성년자, 청년, 청년, ..., 장년, 미성년자, 중년, 중년, NaN]
Length: 12
Categories (5, object): [미성년자 < 청년 < 중년 < 장년 < 노년]

cut 명령이 반환하는 값은 Categorical 클래스 객체이다. 이 객체는 categories 속성으로 라벨 문자열을, codes 속성으로 정수로 인코딩한 카테고리 값을 가진다.

type(cats)
pandas.core.arrays.categorical.Categorical
cats.categories
Index(['미성년자', '청년', '중년', '장년', '노년'], dtype='object')
cats.codes
array([-1,  0,  0,  1,  1,  2,  2,  3,  0,  2,  2, -1], dtype=int8)
df4 = pd.DataFrame(ages, columns=["ages"])
df4["age_cat"] = pd.cut(df4.ages, bins, labels=labels)
df4
ages age_cat
0 0 NaN
1 2 미성년자
2 10 미성년자
3 21 청년
4 23 청년
5 37 중년
6 31 중년
7 61 장년
8 20 미성년자
9 41 중년
10 32 중년
11 101 NaN

따라서 위 데이터프레임의 age_cat 열값은 문자열이 아니다. 이를 문자열로 만들려면 astype 메서드를 사용해야 한다.

df4.age_cat.astype(str) + df4.ages.astype(str)
0       nan0
1      미성년자2
2     미성년자10
3       청년21
4       청년23
5       중년37
6       중년31
7       장년61
8     미성년자20
9       중년41
10      중년32
11    nan101
dtype: object

qcut 명령은 구간 경계선을 지정하지 않고 데이터 갯수가 같도록 지정한 수의 구간으로 나눈다. 예를 들어 다음 코드는 1000개의 데이터를 4개의 구간으로 나누는데 각 구간은 250개씩의 데이터를 가진다.

data = np.random.randn(1000)
cats = pd.qcut(data, 4, labels=["Q1", "Q2", "Q3", "Q4"])
cats
[Q2, Q1, Q2, Q3, Q1, ..., Q1, Q1, Q4, Q4, Q2]
Length: 1000
Categories (4, object): [Q1 < Q2 < Q3 < Q4]
pd.value_counts(cats)
Q4    250
Q3    250
Q2    250
Q1    250
dtype: int64

연습 문제 4.4.7

타이타닉호 승객을 ‘미성년자’, ‘청년’, ‘중년’, ‘장년’, ‘노년’ 나이 그룹으로 나눈다.

bins = [1, 20, 30, 50, 70, 100]
labels = ["미성년자", "청년", "중년", "장년", "노년"]

그리고 각 나이 그룹의 승객 비율을 구한다. 비율의 전체 합은 1이 되어야 한다.

연습 문제 4.4.8

타이타닉호의 승객에 대해 나이와 성별에 의한 카테고리 열인 category3 열을 만들어라. category3 카테고리는 다음과 같이 정의된다.

  1. 20살 미만이면 성별에 관계없이 “미성년자”라고 한다.

  2. 20살 이상이면 나이에 따라 “청년”, “중년”, “장년”, “노년”을 구분하고 그 뒤에 성별을 나타내는 “남성”, “여성”을 붙인다.