[Python] 데이터 사이언스 스쿨 - 4.4 데이터프레임의 데이터 조작

Updated:

데이터 사이언스 스쿨 자료를 토대로 공부한 내용입니다.

실습과정에서 필요에 따라 내용의 누락 및 추가, 수정사항이 있습니다.


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

데이터 갯수 세기

import numpy as np
import pandas as pd
# 시리즈 생성
ps = pd.Series(range(10))
ps[5] = np.nan
ps
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
5    NaN
6    6.0
7    7.0
8    8.0
9    9.0
dtype: float64
  • NaN 값을 가지는 시리즈를 생성하였다.
# NaN 값은 제외하고 카운트
print(ps.count())

# len은 NaN 값을 포함
print(len(ps))
9
10
  • count()는 NaN 값을 제외하고 갯수를 세며 len()은 NaN 값을 포함해서 갯수를 센다.
# 데이터 프레임 생성
np.random.seed(2)
df = pd.DataFrame(np.random.randint(5, size=(4, 4)),
                  index = ["a","b","c","d"],
                  columns = ["A","B","C","D"],
                  dtype=float)
df.iloc[2, 3] = np.nan
df
A B C D
a 0.0 0.0 3.0 2.0
b 3.0 0.0 2.0 1.0
c 3.0 2.0 4.0 NaN
d 4.0 3.0 4.0 2.0
  • NaN 값을 가지는 데이터 프레임을 생성하였다.
# 열 단위로 계산
df.count()
A    4
B    4
C    4
D    3
dtype: int64
  • 데이터 프레임 역시 count()는 NaN 값을 제외하고 갯수를 센다.

  • 기본적으로 열 단위로 계산되는데 이는 axis=0이 디폴트여서이다.

  • axis=1로 설정하면 행 단위로 갯수를 센다.

연습 문제 4.4.1

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

# 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
titanic.count()
survived       891
pclass         891
sex            891
age            714
sibsp          891
parch          891
fare           891
embarked       889
class          891
who            891
adult_male     891
deck           203
embark_town    889
alive          891
alone          891
dtype: int64
  • 각 열별로 891개의 행을 가지는데 age, embarked, deck, embark_town에는 결측이 포함되어 있다.

카테고리 값 세기

# 시리즈 생성
ps2 = pd.Series(["a","b","b","c","e","a","b"],
                 index = range(10,17))
ps2
10    a
11    b
12    b
13    c
14    e
15    a
16    b
dtype: object
# 시리즈 각 값의 개수 확인
ps2.value_counts()
b    3
a    2
e    1
c    1
dtype: int64
  • 넘파이에서 사용한 np.unique( [], return_counts= True), np.bincount()와 같다.

  • value_counts()는 판다스 시리즈에서 사용가능하며 기존 시리즈의 값이 index, 값의 갯수가 values인 시리즈를 생성한다.

# df["A"].value_counts()
# df.A.value_counts()
df.loc[:,"A"].value_counts()
3.0    2
4.0    1
0.0    1
Name: A, dtype: int64
  • 데이터 프레임은 value_counts()가 없어서 각 열별로 추출(시리즈로 변환)해서 확인할 수 있다.

정렬

# value_counts()를 통해 기존 값이 인덱스로, 갯수가 값으로 변형된 새로운 시리즈가 생성
# 인덱스를 기준으로 정렬
ps2.value_counts().sort_index()
a    2
b    3
c    1
e    1
dtype: int64
  • sort_index()는 인덱스를 기준으로 정렬하는 함수이다.
# 값을 기준으로 정렬 - NaN이 마지막에 출력
ps.sort_values()
0    0.0
1    1.0
2    2.0
3    3.0
4    4.0
6    6.0
7    7.0
8    8.0
9    9.0
5    NaN
dtype: float64
  • sort_values()는 값을 기준으로 정렬한다.
# 내림차순 정렬
ps.sort_values(ascending = False)
9    9.0
8    8.0
7    7.0
6    6.0
4    4.0
3    3.0
2    2.0
1    1.0
0    0.0
5    NaN
dtype: float64
  • 내림차순으로 정렬하려면 ascending 옵션을 False로 설정한다.
# 데이터 프레임 정렬
df.sort_values(by="A")
A B C D
a 0.0 0.0 3.0 2.0
b 3.0 0.0 2.0 1.0
c 3.0 2.0 4.0 NaN
d 4.0 3.0 4.0 2.0
  • 데이터 프레임을 정렬하기 위해선 by 옵션으로 기준이 되는 열을 지정해야한다.
# 2순위 정렬 선택
df.sort_values(by=["A","B"], ascending = [True,False])
A B C D
a 0.0 0.0 3.0 2.0
c 3.0 2.0 4.0 NaN
b 3.0 0.0 2.0 1.0
d 4.0 3.0 4.0 2.0
  • 2순위 정렬 기준을 추가하려면 by 옵션에 여려 열을 지정한다.

연습 문제 4.4.2

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

# 성별 인원수
titanic.sex.value_counts().sort_values()
female    314
male      577
Name: sex, dtype: int64
# 나이별 인원수
titanic["age"].value_counts().sort_values()
0.42      1
20.50     1
24.50     1
0.67      1
14.50     1
         ..
30.00    25
19.00    25
18.00    26
22.00    27
24.00    30
Name: age, Length: 88, dtype: int64
  • 나이가 0.42세 등 이상한 값이 있지만 우선 건너띈다.
# 선실별 인원수
titanic.loc[:,"class"].value_counts().sort_values()
Second    184
First     216
Third     491
Name: class, dtype: int64
# 사망/생존 인원수
titanic["alive"].value_counts().sort_values()
yes    342
no     549
Name: alive, dtype: int64

행/열 합계

np.random.seed(1)
df2 = pd.DataFrame(np.random.randint(10, size=(4, 8)),
                   index = ["a","b","c","d"],
                   columns = ["A","B","C","D","E","F","G","H"])
df2
A B C D E F G H
a 5 8 9 5 0 0 1 7
b 6 9 2 4 5 2 4 2
c 4 7 7 9 1 7 0 6
d 9 9 7 6 9 1 0 1
# 열 합계 (디폴트 axis = 0)
df2.sum()
A    24
B    33
C    25
D    24
E    15
F    10
G     5
H    16
dtype: int64
  • sum()과 같은 대부분의 축소 연산은 디폴트 axis=0으로 열 단위 연산을 한다.
# 행 합계
df2.sum(axis = 1)
a    35
b    34
c    41
d    42
dtype: int64
  • 행 단위 연산을 할 때는 axis=1 옵션을 부여한다.
# 행 추가
df2.loc["colTotal",:] = df2.sum()
df2
A B C D E F G H
a 5.0 8.0 9.0 5.0 0.0 0.0 1.0 7.0
b 6.0 9.0 2.0 4.0 5.0 2.0 4.0 2.0
c 4.0 7.0 7.0 9.0 1.0 7.0 0.0 6.0
d 9.0 9.0 7.0 6.0 9.0 1.0 0.0 1.0
colTotal 24.0 33.0 25.0 24.0 15.0 10.0 5.0 16.0
# 열 추가
df2["RowSum"] = df2.sum(axis=1)
df2
A B C D E F G H RowSum
a 5.0 8.0 9.0 5.0 0.0 0.0 1.0 7.0 35.0
b 6.0 9.0 2.0 4.0 5.0 2.0 4.0 2.0 34.0
c 4.0 7.0 7.0 9.0 1.0 7.0 0.0 6.0 41.0
d 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

연습 문제 4.4.3

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

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

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

# 1. 타이타닉호 승객의 평균 나이
# titanic.mean()
a1 = round( titanic.age.mean(), 0)

print(f"평균 나이: {a1}세")
평균 나이: 30.0세
# 2. 타이타닉호 승객 중 여성 승객의 평균 나이
titanic_female = titanic[titanic["sex"] == "female"]

a2 = round(titanic_female["age"].mean(), 0)
print(f"평균 나이: {a2}세")
평균 나이: 28.0세
# 3. 타이타닉호 승객 중 1등신 선실의 여성 승객의 평균 나이
titanic_first_female = titanic[(titanic["class"] == "First") & (titanic["sex"] == "female")]

a3 = round(titanic_first_female["age"].mean(), 0)
print(f"평균 나이: {a3}세")
평균 나이: 35.0세

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
# 열 단위 연산
# x는 데이터 프레임을 받으며 각 열별로 뒤 함수를 적용
df3.apply(lambda x: x.max() - x.min())
A    3
B    2
C    4
dtype: int64
  • 위 예시는 각 열별로 최대값 - 최소값을 반환한다.

  • 디폴트는 axis=0으로 열단위 연산을 수행한다.

# 행 단위 연산
# x는 데이터 프레임을 받으며 각 행별로 뒤 함수를 적용
df3.apply(lambda x: x.max() - x.min(), axis = 1)
0    1
1    2
2    3
3    2
4    1
dtype: int64
  • 위 예시는 axis=1 옵션을 부여하여서 각 행별로 최대값 - 최소값을 반환한다.
temp = pd.DataFrame({
      'A': [1, 3, 4, 3, "a"],
      'B': ["a", "b", "c", "d", "f"],
      'C': [1, 5, 2, 4, 4]
})

temp
A B C
0 1 a 1
1 3 b 5
2 4 c 2
3 3 d 4
4 a f 4
# 각 열마다 사용된 값의 갯수
temp.apply(pd.value_counts)
A B C
1 1.0 NaN 1.0
2 NaN NaN 1.0
3 2.0 NaN NaN
4 1.0 NaN 2.0
5 NaN NaN 1.0
a 1.0 1.0 NaN
b NaN 1.0 NaN
c NaN 1.0 NaN
d NaN 1.0 NaN
f NaN 1.0 NaN
  • 이번엔 apply()pd.value_counts()를 적용하였다.

  • 각 열에 대해 어떤 값이 얼마나 사용되었는지를 확인 가능하다.

  • 하지만 기존 모든 열의 값이 새로운 index로 반환되서 이전 시리즈에서 적용하던 것처럼 분리해서 보는게 좋아 보인다.

# 20살을 기준으로 성인/아이 구분
titanic["adult/child"] = titanic.apply(lambda x: "adult" if x.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
  • 조금 더 복잡한 계산으로 if를 이용해서 apply()를 적용 가능하다.

  • apply(lambda x: 값1 if 조건1 값2 if 조건2 .... else 나머지 값, axis =1) 형태로 지정하면 부여 받는 x에 대해 조건별로 값을 반환한다.

  • 여기선 각 행(사람)마다 성인/아이를 구분하므로 axis=1 옵션을 부여하였다.

연습 문제 4.4.4

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

category1 카테고리는 다음과 같이 정의된다.

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

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

titanic["category1"] = titanic.apply(lambda x: x.sex if x.age >= 20 else "child", axis=1)
titanic.tail(10)
survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone adult/child category1
881 0 3 male 33.0 0 0 7.8958 S Third man True NaN Southampton no True adult male
882 0 3 female 22.0 0 0 10.5167 S Third woman False NaN Southampton no True adult female
883 0 2 male 28.0 0 0 10.5000 S Second man True NaN Southampton no True adult male
884 0 3 male 25.0 0 0 7.0500 S Third man True NaN Southampton no True adult male
885 0 3 female 39.0 0 5 29.1250 Q Third woman False NaN Queenstown no False adult female
886 0 2 male 27.0 0 0 13.0000 S Second man True NaN Southampton no True adult male
887 1 1 female 19.0 0 0 30.0000 S First woman False B Southampton yes True child child
888 0 3 female NaN 1 2 23.4500 S Third woman False NaN Southampton no False child child
889 1 1 male 26.0 0 0 30.0000 C First man True C Cherbourg yes True adult male
890 0 3 male 32.0 0 0 7.7500 Q Third man True NaN Queenstown no True adult male

fillna 메서드

fillna()는 NaN 값을 원하는 값으로 변환한다.

# 데이터 프레임 생성
temp = pd.DataFrame({
      'A': [1, 3, 4, 3, "a"],
      'B': ["a", "b", "c", "d", "f"],
      'C': [1, 5, 2, 4, 4]
})

# 각 열별 데이터 갯수
temp2 = temp.apply(pd.value_counts)
temp2
A B C
1 1.0 NaN 1.0
2 NaN NaN 1.0
3 2.0 NaN NaN
4 1.0 NaN 2.0
5 NaN NaN 1.0
a 1.0 1.0 NaN
b NaN 1.0 NaN
c NaN 1.0 NaN
d NaN 1.0 NaN
f NaN 1.0 NaN
temp2.fillna("결측")
A B C
1 1 결측 1
2 결측 결측 1
3 2 결측 결측
4 1 결측 2
5 결측 결측 1
a 1 1 결측
b 결측 1 결측
c 결측 1 결측
d 결측 1 결측
f 결측 1 결측
  • NaN 값이 모두 지정한 “결측”이라는 문자로 변경되었다.

연습 문제 4.4.5

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

# 현재 고객의 평균나이
mean_age = titanic["age"].mean()

# 시리즈에서도 fillna는 적용 되므로 다음과 같이 작성
titanic["age"] = titanic["age"].fillna(mean_age)
titanic.isna().sum()
survived         0
pclass           0
sex              0
age              0
sibsp            0
parch            0
fare             0
embarked         2
class            0
who              0
adult_male       0
deck           688
embark_town      2
alive            0
alone            0
adult/child      0
category1        0
dtype: int64
  • age의 결측값이 0으로 모두 대체되었음을 확인 가능하다.

astype 메서드

astype()은 판다스 시리즈, 데이터 프레임의 자료형을 변경해준다.

일반적인 int(), str() 등을 사용하면 에러가 발생한다.

temp2.fillna(0)
A B C
1 1.0 0.0 1.0
2 0.0 0.0 1.0
3 2.0 0.0 0.0
4 1.0 0.0 2.0
5 0.0 0.0 1.0
a 1.0 1.0 0.0
b 0.0 1.0 0.0
c 0.0 1.0 0.0
d 0.0 1.0 0.0
f 0.0 1.0 0.0
# astype을 이용해 자료형 변경
temp2.fillna(0).astype(int)
A B C
1 1 0 1
2 0 0 1
3 2 0 0
4 1 0 2
5 0 0 1
a 1 1 0
b 0 1 0
c 0 1 0
d 0 1 0
f 0 1 0
  • 기존 float형이 int형으로 잘 변경되었다.

연습 문제 4.4.6

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

category2 카테고리는 다음과 같이 정의된다.

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

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

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

# 방법 1 - astype()
titanic["category2"] = titanic["sex"] + titanic["age"].astype(int).astype(str)
titanic[["category2"]]
category2
0 male22
1 female38
2 female26
3 female35
4 male35
... ...
886 male27
887 female19
888 female29
889 male26
890 male32

891 rows × 1 columns

# 방법 2 - apply()
titanic["category2"] = titanic.apply(lambda x: x.sex + str(int(x.age)), axis = 1)
titanic[["category2"]]
category2
0 male22
1 female38
2 female26
3 female35
4 male35
... ...
886 male27
887 female19
888 female29
889 male26
890 male32

891 rows × 1 columns

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

ages = [0, 2, 10, 21, 23, 37, 31, 61, 20, 41, 32, 101]
bins = [1, 20, 30, 50, 70, 100]
labels = ["미성년자", "청년", "중년", "장년", "노년"]
cats = pd.cut(ages, bins, labels=labels)
cats
[NaN, '미성년자', '미성년자', '청년', '청년', ..., '장년', '미성년자', '중년', '중년', NaN]
Length: 12
Categories (5, object): ['미성년자' < '청년' < '중년' < '장년' < '노년']
  • pd.cut()은 데이터를 지정한 구간(bins)에 따라 지정한 라벨(labels)로 변경해준다.

  • 만약 데이터 값 중 지정한 구간외에 값이 있다면 NaN을 반환한다.

# Categorical 클래스
type(cats)
pandas.core.arrays.categorical.Categorical
  • pd.cut()으로 생성한 객체는 pandas.core.arrays.categorical.Categorical 클래스로 지정되어 있다.
cats.categories
Index(['미성년자', '청년', '중년', '장년', '노년'], dtype='object')
  • categories 속성을 이용해서 라벨 문자열 종류를 확인 가능하다.
cats.codes
array([-1,  0,  0,  1,  1,  2,  2,  3,  0,  2,  2, -1], dtype=int8)
  • codes 속성을 이용해서 각 데이터가 몇 번째 라벨의 값인지 위치를 확인 가능하다.

  • 이 예제에선 라벨이 [“미성년자”, “청년”, “중년”, “장년”, “노년”, NaN]으로 설정돼 미성년자는 0으로 반환된다.

# 데이터 프레임 생성
df4 = pd.DataFrame(ages, columns=["ages"])

# 카테고리 값 추가
df4["age_cat"] = pd.cut(df4.ages, bins, labels=labels)
print("age_cat 자료형:", df4.age_cat.dtype)
df4
age_cat 자료형: category
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
  • 조금 더 응용하면 위 예시와 같이 데이터 프레임에 카테고리 열을 추가 가능하다.

  • 물론 apply()로 if문을 이용해서도 생성가능하지만 구간이 클수록 코드가 길어질 것 이다.

  • 마지막으로 age_cat은 pd.cut()으로 생성한 열이므로 자료형은 category이다.

df4.age_cat.astype(str) + "-" + df4.ages.astype(str)
0       nan-0
1      미성년자-2
2     미성년자-10
3       청년-21
4       청년-23
5       중년-37
6       중년-31
7       장년-61
8     미성년자-20
9       중년-41
10      중년-32
11    nan-101
dtype: object
  • 앞서 자료형이 category였으므로 문자형으로 바꾸고 싶다면 astype()을 사용한다.
data = np.random.randn(1000)

# qcut은 데이터 갯수가 동일하게 끔 N개의 구간으로 나눔
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.qcut()pd.cut()과 비슷하지만 구간을 직접 지정하지 않고 구간 수를 지정한다.

  • 명령어를 실행하면 각 구간별로 데이터의 갯수가 같게끔 데이터를 나눈다.

# 데이터 갯수가 동일한 것 확인
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이 되어야 한다.

# 구간, 라벨 설정
bins = [1, 20, 30, 50, 70, 100]
labels = ["미성년자", "청년", "중년", "장년", "노년"]

# 그룹 추가
titanic["age_g"] = pd.cut(titanic.age, bins, labels = labels).astype(str)
titanic[ ["age", "age_g"] ]
age age_g
0 22.000000 청년
1 38.000000 중년
2 26.000000 청년
3 35.000000 중년
4 35.000000 중년
... ... ...
886 27.000000 청년
887 19.000000 미성년자
888 29.699118 청년
889 26.000000 청년
890 32.000000 중년

891 rows × 2 columns

# 시리즈로 분리 - 그룹별 갯수
temp4 = titanic["age_g"].value_counts()

# 비율 생성
temp5 = temp4 / temp4.sum()
temp5
청년      0.456790
중년      0.270483
미성년자    0.185185
장년      0.066218
nan     0.015713
노년      0.005612
Name: age_g, dtype: float64
# 비율 합계
print("비율 합계:", temp5.sum())
비율 합계: 1.0

연습 문제 4.4.8

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

category3 카테고리는 다음과 같이 정의된다.

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

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

# 구간, 라벨 설정
bins = [20, 30, 50, 70, 100]
labels = ["청년", "중년", "장년", "노년"]
# age_g는 연습문제 4.4.7에서 생성
titanic["category3"] = titanic.apply(lambda x: "미성년자" if x.age < 20 
                                     else x.age_g + ": " + "남성" if x.sex == "male"
                                     else x.age_g + ": " + "여성", axis=1)
titanic[["age","age_g","category3"]].tail()
age age_g category3
886 27.000000 청년 청년: 남성
887 19.000000 미성년자 미성년자
888 29.699118 청년 청년: 여성
889 26.000000 청년 청년: 남성
890 32.000000 중년 중년: 남성

Leave a comment