당뇨병 예측하기

Reddit 에서 흥미로운 글을 봤어요. 인공신경망을 이용해서 당뇨를 예측하는 튜토리얼이었죠. 파이썬을 이용해서 15줄의 코드가 전부였습니다.

0. 기계학습이라고?

2016년도 이세돌을 이겼던 AlphaGO를 기억하시나요? AlphaGO라는 인공지능을 바로 기계학습으로 만들었습니다.

기계 학습 또는 머신 러닝(영어: machine learning)은 인공 지능의 한 분야로, 컴퓨터가 학습할 수 있도록 하는 알고리즘과 기술을 개발하는 분야를 말한다. 가령, 기계 학습을 통해서 수신한 이메일이 스팸인지 아닌지를 구분할 수 있도록 훈련할 수 있다. - wikipedia

모든 경우의 수를 사람이 코딩할 필요가 없이 최소한의 것만 해주면, 인공 지능이 알아서 한다. 그것이 기계학습의 매력이죠. 기계학습에도 여러 종류가 있습니다. 우리가 이번에 사용할 것은 인공 신경망이라는 것입니다.

인공신경망

인공신경망(artificial neural network, ANN)은 기계학습과 인지과학에서 생물학의 신경망(동물의 중추신경계중 특히 뇌)에서 영감을 얻은 통계학적 학습 알고리즘이다. - wikipedia

어떻게 인공신경망을 만드냐고요? 파이썬에는 이미 기계학습에 사용되는 프레임워크(예를 들면 TensorFlow)가 존재해서, 우리는 그냥 쓰기만 하면되요!

1. Keras를 사용할 거에요.

keras

Keras is a high-level neural networks API, written in Python and capable of running on top of TensorFlow, CNTK, or Theano. It was developed with a focus on enabling fast experimentation.

Keras는 다음과 같은 특징이 있습니다.

  • 쉽게 그리고 빠르게 코딩이 가능
  • CPU 와 GPU 자원을 문제없이 사용
  • 파이썬 2.7 - 3.6을 지원

자세한 내용은 공식 사이트를 방문하세요.

데이터 세트 설명

사용한 데이터 세트는 Pima Indians Diabetes Data Set 입니다. 출처는 링크를 확인하세요.

미국 원주민 중 Pima 족은 원래는 당뇨병의 유병율이 적었는데, 서구식 식습관에 의해 급속도로 당뇨병에 걸렸습니다. 그래서 당뇨병을 연구하는데 귀중한 데이터로 사용되고 있습니다.

데이터 세트는 CSV 포멧이고 총 9가지의 속성으로 구성되어 있습니다.

  1. 임신횟수
  2. 글루코스 내성(glucose tolerance)실험 후의 혈당수치
  3. 확장기 혈압(mm Hg)
  4. 상완 삼두근 피부 두께 (mm)
  5. 혈액내 인슐린 수치 (mu U/ml)
  6. BMI(비만도) 수치 (weight in kg/(height in m)^2)
  7. 당뇨병 가족력: 계산식은 데이터세트의 출처에서 확인하세요.
  8. 나이 (years)
  9. 당뇨병 여부 (0 or 1; 1은 발병)

출처

원작자의 코드를 확인하고 싶다면 다음 링크를 확인하세요.

2. 이제 실습을 해봅시다.

In [2]:
# 필요한 모듈을 불러들입니다.
from keras.models import Sequential
from keras.layers import Dense
import numpy
from urllib import request
import matplotlib.pyplot as plt

%matplotlib inline

# 데이터 세트의 URL을 설정
url = "http://archive.ics.uci.edu/ml/machine-learning-databases/pima-indians-diabetes/pima-indians-diabetes.data"
f = request.urlopen(url)

# random seed for reproducibility
numpy.random.seed(2)

# 데이터 세트를 불러옵니다.
dataset = numpy.loadtxt(f, delimiter=",")
Using TensorFlow backend.

이걸로 데이터 세트는 준비가 되었습니다.

모델 만들기

다음으로 Keras를 이용해 간단한 모델을 만들고, 800번의 학습(epochs)을 해보겠습니다. 800번 정도 실행시키면 시간이 좀 걸립니다. 기다려 주세요.

In [3]:
# 데이터세트를 두 가지 원인(X) 과 결과(Y)로 나누어 줍니다.
X = dataset[:, 0:8]
Y = dataset[:, 8]

# create model, add dense layers one by one specifying activation function
model = Sequential()
model.add(
    Dense(12, input_dim=8, activation="relu")
)  # input layer requires input_dim param
model.add(Dense(15, activation="relu"))
model.add(Dense(8, activation="relu"))
model.add(Dense(10, activation="relu"))
model.add(
    Dense(1, activation="sigmoid")
)  # sigmoid instead of relu for final probability between 0 and 1

# compile the model, adam gradient descent (optimized)
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=["accuracy"])

# call the function to fit to the data (training the network)
# verbose=0 는 프로그래스를 숨깁니다.
history = model.fit(X, Y, epochs=800, batch_size=10, verbose=0)

# 모델의 정확도를 계산합니다.
scores = model.evaluate(X, Y)
print("\n%s: %.2f%%" % (model.metrics_names[1], scores[1] * 100))
768/768 [==============================] - 0s 80us/step

acc: 87.89%

우리의 모델이 학습결과 87.89% 라는 정확도를 가지게 되었습니다.

2. 시각화하기

이제 다 끝났습니다. 다음으로는 Keras가 학습을 반복할 때마다 정확도가 얼마나 올라가는지 시각화를 해보겠습니다.

In [3]:
# Get the figure and the axes
fig, (ax0, ax1) = plt.subplots(nrows=1, ncols=2, sharey=False, figsize=(10, 5))

# 모델의 정확도를 그립니다.
ax0.plot(history.history["acc"])
ax0.set(title="model accuracy", xlabel="epoch", ylabel="accuracy")

# 모델의 오차를 그립니다.
ax1.plot(history.history["loss"])
ax1.set(title="model loss", xlabel="epoch", ylabel="loss")
Out[3]:
[Text(0,0.5,'loss'), Text(0.5,0,'epoch'), Text(0.5,1,'model loss')]
No description has been provided for this image

결과를 보니, 한 800번 정도 학습하면 꽤 정확한 모델을 얻을 수 있네요.

3. 마치며,

모델을 만들었다면, 이제 모델을 유용하게 사용할 시간입니다. 가상의 환자 데이터를 만들어 그 환자가 당뇨에 걸릴 확률을 알아보겠습니다.

patient_1

  1. 임신한 적 없음
  2. 글루코스 내성(glucose tolerance)실험 후의 혈당수치는 137
  3. 확장기 혈압은 90(mm Hg)
  4. 상완 삼두근 피부 두께는 35(mm)
  5. 혈액내 인슐린 수치는 168 (mu U/ml)
  6. BMI(비만도) 수치는 43.1
  7. 당뇨병 가족력은 2.288
  8. 나이 (years)는 33살

이라고 가정합니다.

In [4]:
# 가상의 환자 데이터 입력
patient_1 = numpy.array([[0, 137, 90, 35, 168, 43.1, 2.288, 33]])

# 모델로 예측하기
prediction = model.predict(patient_1)

# 예측결과 출력하기
print(prediction * 100)
[[30.039745]]

우리의 모델에 따르면, patient_1는 당뇨병에 걸릴 확률이 약 30% 입니다.

깔끔한 데이터(Tidy data)

깔끔한 데이터(Tidy data)란?

우리는 데이터 분석을 수행하면서 다양한 데이터 변환 작업을 수행하게 된다. 이는 데이터가 원래 특정 분석을 염두에 두고 만들어지는 경우가 거의 없기 때문이며, 사실 애초 데이터 설계를 할 때 분석 목적을 알기도 불가능하다는 게 가장 큰 원인이 아닐까 한다. 이런 연유로 전체 데이터 분석 작업에서 70% 혹은 80% 이상이 이런 데이터 변환 및 전처리 작업에서 소모된다. 1

다른 사람들과 데이터(예를 들면 엑셀파일)를 주고 받다 보면 깨닫게 됩니다. 모든 사람의 생각은 다르다는 것을요. 사람마다 (좋게 말하면) 각양각색으로 정리를 하다보니, 여러명이 작성한 엑셀 파일을 하나의 파일로 합치려 보면 결국에는 누군가가 새로 정리를 해야 하는 상황이 됩니다.

Tidy data의 개념은 그런 상황을 방지하기 위해 Jeff Leek이 만들었습니다. 데이터 처리에 가장 많은 시간이 소요되는 전처리 과정을 줄여 보자, 그러기 위해 처음부터 데이터를 표준화해서 만들자는 것입니다. 처음부터 기본적인 규칙을 가지고 데이터를 만들면, 모두의 시간을 아낄 수 있으니까요.

위키피디아에 따르면 Tidy data의 정의는 다음와 같습니다. "밑바닥 부터 시작할 필요 없는 데이터"

Tidy data there’s no need to start from scratch. -from wiki

더 알아 보기

  • 2014년도에 출판된 논문 입니다.
  • 데이터의 출처는 이곳 입니다.

깔끔한 데이터(Tidy data)의 특징

Jeff Leek가 쓴 책 The Elements of Data Analytic Style에서 정의한 깔끔한 데이터는 아래와 같은 특징을 가집니다.

  1. 각 변수는 개별의 열(column)으로 존재한다.
  2. 각 관측치는 행(row)를 구성한다.
  3. 각 표는 단 하나의 관측기준에 의해서 조직된 데이터를 저장한다.
  4. 만약 여러개의 표가 존재한다면, 적어도 하나이상의 열(column)이 공유되어야 한다.
  • 변수(Variable): 예를 들면 키, 몸무게, 성별
  • 값(Value): 예를 들자면 152 cm, 80 kg, 여성
  • 관측치(Observation): 값을 측정한 단위, 여기서는 각각의 사람

너무 복잡하다고 생각되신다면 아래 예시를 확인하세요.

지저분한 데이터의 예:

Treatment A Treatment B
John Smith - 2
Jane Doe 16 11
Mary Johnson 3 1

깔끔한 데이터(Tidy data)의 예:

Name Treatment Result
John Smith a -
Jane Doe a 16
Mary Johnson a 3
John Smith b 2
Jane Doe b 11
Mary Johnson b 1

지저분한 데이터 처리하기

Wickham논문에 있던 데이터를 가지고 아래의 문제를 해결해 보도록 하겠습니다. 여기서 우리의 목표는 데이터 분석이 아니고 깔끔한 데이터를 만드는 것임을 명심하세요.

지저분한 데이터의 일반적인 모습은 다음과 같습니다.

  1. 열 이름(Column header)이 변수 이름이 아니고 값인 경우
  2. 같은 표에 다양한 관측 단위(observational units)가 있는 경우
  3. 하나의 열(column)에 여러 값이 들어 있는 경우
  4. 변수가 행과 열에 모두 포함되어 있는 경우
  5. 하나의 관측 단위(observational units)가 여러 파일로 나누어져 있는 경우

우리는 Python의 대표적인 라이브러리인 Pandas를 사용하도록 할게요.

In [6]:
# 필요한 라이브러리 불러들이기
import pandas as pd
import datetime
import glob
import re  # 정규식

1. 열 이름(Column header)이 변수 이름이 아니고 값인 경우

Pew Research Center Dataset

종교에 따른 개인의 수입의 관한 데이터입니다. 먼저 pandasread_csv기능을 사용해 파일을 읽어옵니다.

In [7]:
df = pd.read_csv("./data/pew-raw.csv")
df
Out[7]:
religion <$10k $10-20k $20-30k $30-40k $40-50k $50-75k
0 Agnostic 27 34 60 81 76 137
1 Atheist 12 27 37 52 35 70
2 Buddhist 27 21 30 34 33 58
3 Catholic 418 617 732 670 638 1116
4 Dont know/refused 15 14 15 11 10 35
5 Evangelical Prot 575 869 1064 982 881 1486
6 Hindu 1 9 7 9 11 34
7 Historically Black Prot 228 244 236 238 197 223
8 Jehovahs Witness 20 27 24 24 21 30
9 Jewish 19 19 25 25 30 95
  • Evangelical Prot는 기독교 종파중에 하나로 '개신교'입니다.

문제점: 이 데이터들의 문제는 열 이름(columns headers)이 개인소득의 범위로 되어 있다는 것이죠.

다시 말해서 보기에는 좋아보일지는 몰라도 분석하기에는 어려운 형식입니다.

이러한 데이터를 Tidy data 형태로 변환하기 위해서 pandas라이브러리에서는 아주 쉬운 기능을 제공해 줍니다. 바로 melt라는 기능이지요. pivot table의 반대되는 개념으로 행이 많은 데이터를 열이 많은 데이터로 바꿔줍니다. melt는 아주 유용하기 때문에 앞으로도 자주 언급 됩니다.

In [8]:
formatted_df = pd.melt(df, ["religion"], var_name="income", value_name="freq")
formatted_df = formatted_df.sort_values(by=["religion"])  # 종교 이름순으로 정렬
formatted_df.head(10)  # 너무 길기 때문에 윗쪽 10개만 보겠습니다.
Out[8]:
religion income freq
0 Agnostic <$10k 27
30 Agnostic $30-40k 81
40 Agnostic $40-50k 76
50 Agnostic $50-75k 137
10 Agnostic $10-20k 34
20 Agnostic $20-30k 60
41 Atheist $40-50k 35
21 Atheist $20-30k 37
11 Atheist $10-20k 27
31 Atheist $30-40k 52

이것이 Pew Research Center Dataset 의 Tidy data 형태 입니다.

Billboard Top 100 Dataset

이 데이터는 아주 오래전 같은 1999년부터 2000년까지의 빌보드차트 주간 순위 변동을 포함하고 있는 데이터 입니다.

In [69]:
df = pd.read_csv("./data/billboard.csv", encoding="mac_latin2")
df.head(10)
Out[69]:
year artist.inverted track time genre date.entered date.peaked x1st.week x2nd.week x3rd.week ... x67th.week x68th.week x69th.week x70th.week x71st.week x72nd.week x73rd.week x74th.week x75th.week x76th.week
0 2000 Destiny's Child Independent Women Part I 3:38 Rock 2000-09-23 2000-11-18 78 63.0 49.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 2000 Santana Maria, Maria 4:18 Rock 2000-02-12 2000-04-08 15 8.0 6.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 2000 Savage Garden I Knew I Loved You 4:07 Rock 1999-10-23 2000-01-29 71 48.0 43.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 2000 Madonna Music 3:45 Rock 2000-08-12 2000-09-16 41 23.0 18.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4 2000 Aguilera, Christina Come On Over Baby (All I Want Is You) 3:38 Rock 2000-08-05 2000-10-14 57 47.0 45.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
5 2000 Janet Doesn't Really Matter 4:17 Rock 2000-06-17 2000-08-26 59 52.0 43.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
6 2000 Destiny's Child Say My Name 4:31 Rock 1999-12-25 2000-03-18 83 83.0 44.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
7 2000 Iglesias, Enrique Be With You 3:36 Latin 2000-04-01 2000-06-24 63 45.0 34.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
8 2000 Sisqo Incomplete 3:52 Rock 2000-06-24 2000-08-12 77 66.0 61.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
9 2000 Lonestar Amazed 4:25 Country 1999-06-05 2000-03-04 81 54.0 44.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

10 rows × 83 columns

문제점:

  • 열 이름(columns headers)이 값으로 구성되어 있다: x1st.week, x2st.week 등등
  • 순위가 100위권 밖으로 밀려나게 되면 Nan 값을 가지고, 따라서 필요없는 부수적인 값이 많아진다.

위 데이터의 깔끔한 데이터를 만들기 위해서는 다시 melt 기능을 사용하겠습니다. 각각의 열이 행이 되도록 하고, 순위가 100위 밖으로 밀려난 경우(Nan값을 갖는경우)에는 행을 삭제할게요.

In [70]:
# Melting
id_vars = [
    "year",
    "artist.inverted",
    "track",
    "time",
    "genre",
    "date.entered",
    "date.peaked",
]
df = pd.melt(frame=df, id_vars=id_vars, var_name="week", value_name="rank")

# Formatting
df["week"] = (
    df["week"].str.extract("(\d+)", expand=False).astype(int)
)  # 정규식으로 x1st.week 에서 숫자 1만 추출
df["rank"] = df["rank"].astype(int)

# 필요없는 행을 삭제합니다.
df = df.dropna()

# Create "date" columns
df["date"] = (
    pd.to_datetime(df["date.entered"])
    + pd.to_timedelta(df["week"], unit="w")
    - pd.DateOffset(weeks=1)
)

df = df[["year", "artist.inverted", "track", "time", "genre", "week", "rank", "date"]]
df = df.sort_values(
    ascending=True, by=["year", "artist.inverted", "track", "week", "rank"]
)

# Assigning the tidy dataset to a variable for future usage
billboard = df

df.head(10)
Out[70]:
year artist.inverted track time genre week rank date
246 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 1 87 2000-02-26
563 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 2 82 2000-03-04
880 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 3 72 2000-03-11
1197 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 4 77 2000-03-18
1514 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 5 87 2000-03-25
1831 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 6 94 2000-04-01
2148 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 7 99 2000-04-08
287 2000 2Ge+her The Hardest Part Of Breaking Up (Is Getting Ba... 3:15 R&B 1 91 2000-09-02
604 2000 2Ge+her The Hardest Part Of Breaking Up (Is Getting Ba... 3:15 R&B 2 87 2000-09-09
921 2000 2Ge+her The Hardest Part Of Breaking Up (Is Getting Ba... 3:15 R&B 3 92 2000-09-16

위와 같이 데이터를 깔끔하게 만들었습니다. 하지만 track, time, genre 열을 보시면 아주 많은 중복이 있는 것을 알 수 있습니다. 이러한 점을 해결 하는 방법은 다음 예제에서 다루어 보겠습니다.

2. 하나의 표에 여러가지 타입

위에서 다루었던 빌보드차트 데이터를 가지고 데이터가 반복되는 문제를 해결해 보겠습니다.

묹제점:

  • 다양한 관측 단위(observational units), 여기서는 songrank가 하나의 표에 들어 있습니다. 이를 위해서는 표를 나눌 필요가 있습니다.

먼저, 각각의 노래의 자세한 내용을 담고 있는 표를 만들어 보겠습니다. 그런다음 각각의 song_id를 부여합니다. 그런다음 순위 값을 가지고 있는 표를 song_id로 정리합니다.

In [11]:
songs_cols = ["year", "artist.inverted", "track", "time", "genre"]
songs = billboard[songs_cols].drop_duplicates()
songs = songs.reset_index(drop=True)
songs["song_id"] = songs.index
songs.head(10)
Out[11]:
year artist.inverted track time genre song_id
0 2000 2 Pac Baby Don't Cry (Keep Ya Head Up II) 4:22 Rap 0
1 2000 2Ge+her The Hardest Part Of Breaking Up (Is Getting Ba... 3:15 R&B 1
2 2000 3 Doors Down Kryptonite 3:53 Rock 2
3 2000 3 Doors Down Loser 4:24 Rock 3
4 2000 504 Boyz Wobble Wobble 3:35 Rap 4
5 2000 98° Give Me Just One Night (Una Noche) 3:24 Rock 5
6 2000 A*Teens Dancing Queen 3:44 Pop 6
7 2000 Aaliyah I Don't Wanna 4:15 Rock 7
8 2000 Aaliyah Try Again 4:03 Rock 8
9 2000 Adams, Yolanda Open My Heart 5:30 Gospel 9

위 와같은 새로운 표를 분리하고, 아래와 같이 순위를 포함하고 있는 표를 새로 만들어줍니다.

두개의 표를 연결하기 위해 song_id열을 만드는 것을 주의하세요

In [12]:
ranks = pd.merge(
    billboard, songs, on=["year", "artist.inverted", "track", "time", "genre"]
)
ranks = ranks[["song_id", "date", "rank"]]
ranks.head(10)
Out[12]:
song_id date rank
0 0 2000-02-26 87
1 0 2000-03-04 82
2 0 2000-03-11 72
3 0 2000-03-18 77
4 0 2000-03-25 87
5 0 2000-04-01 94
6 0 2000-04-08 99
7 1 2000-09-02 91
8 1 2000-09-09 87
9 1 2000-09-16 92

3. 다양한 변수가 하나의 열에 있는 경우 Multiple variables stored in one column

Tubercolosis Example

WHO(World Health Organization)에서 수집한 결핵환자의 기록입니다. 이 데이터에는 확인된 결핵환자의 국가, 연도, 나이, 성별이 포함되어 있습니다.

문제점:

  • 몇개의 열(columns)에 다양한 변수가 포함되어 있습니다.(성별과 나이)
  • 값이 존재하지 않는 곳에 NaN과 0 이 혼재되어 있습니다.

미리 알아둘 점:

  • 열의 이름에 적혀있는 "m"이나 "f"는 성별을 뜻합니다.
  • 열의 이름에 적혀있는 숫자는 나이대("0-14","15-24", "25-34", "45-54", "55-64", "65", "unknown")를 나타냅니다.
In [49]:
df = pd.read_csv("./data/tb-raw.csv")
df
Out[49]:
country year m014 m1524 m2534 m3544 m4554 m5564 m65 mu f014
0 AD 2000 0.0 0.0 1.0 0.0 0 0 0.0 NaN NaN
1 AE 2000 2.0 4.0 4.0 6.0 5 12 10.0 NaN 3.0
2 AF 2000 52.0 228.0 183.0 149.0 129 94 80.0 NaN 93.0
3 AG 2000 0.0 0.0 0.0 0.0 0 0 1.0 NaN 1.0
4 AL 2000 2.0 19.0 21.0 14.0 24 19 16.0 NaN 3.0
5 AM 2000 2.0 152.0 130.0 131.0 63 26 21.0 NaN 1.0
6 AN 2000 0.0 0.0 1.0 2.0 0 0 0.0 NaN 0.0
7 AO 2000 186.0 999.0 1003.0 912.0 482 312 194.0 NaN 247.0
8 AR 2000 97.0 278.0 594.0 402.0 419 368 330.0 NaN 121.0
9 AS 2000 NaN NaN NaN NaN 1 1 NaN NaN NaN

이 데이터를 정리하기 위해서는 먼저 melt를 이용해 sex + age group 를 합쳐서 하나의 행으로 만들겠습니다. 그런 다음에 다시 행을 sex, age로 구분해서 정리하도록 하죠.

In [50]:
df = pd.melt(
    df, id_vars=["country", "year"], value_name="cases", var_name="sex_and_age"
)

# Extract Sex, Age lower bound and Age upper bound group
tmp_df = df["sex_and_age"].str.extract("(\D)(\d+)(\d{2})", expand=False)

# Name columns
tmp_df.columns = ["sex", "age_lower", "age_upper"]

# Create `age`column based on `age_lower` and `age_upper`
tmp_df["age"] = tmp_df["age_lower"] + "-" + tmp_df["age_upper"]

# Merge
df = pd.concat([df, tmp_df], axis=1)

# Drop unnecessary columns and rows
df = df.drop(["sex_and_age", "age_lower", "age_upper"], axis=1)
df = df.dropna()
df = df.sort_values(ascending=True, by=["country", "year", "sex", "age"])
df.head(10)
Out[50]:
country year cases sex age
0 AD 2000 0.0 m 0-14
10 AD 2000 0.0 m 15-24
20 AD 2000 1.0 m 25-34
30 AD 2000 0.0 m 35-44
40 AD 2000 0.0 m 45-54
50 AD 2000 0.0 m 55-64
81 AE 2000 3.0 f 0-14
1 AE 2000 2.0 m 0-14
11 AE 2000 4.0 m 15-24
21 AE 2000 4.0 m 25-34

이것이 정리된 결과 입니다.

4. 변수가 행과 열에(rows and columns) 모두 포함되어 있는 경우

Global Historical Climatology Network Dataset

이것은 2010년도 멕시코 기상청(MX17004)에서 5개월 동안 측정한 기상 데이터입니다.

In [24]:
df = pd.read_csv("./data/weather-raw.csv")

문제점:

  • 변수들이 행(tmin, tmax) 과 열(days)에 존재합니다.

깔끔한 데이터를 만들기 위해 melt기능을 사용해 day_raw를 각각의 열로 만들겠습니다.

In [25]:
df = pd.melt(df, id_vars=["id", "year", "month", "element"], var_name="day_raw")
df.head(10)
Out[25]:
id year month element day_raw value
0 MX17004 2010 1 tmax d1 NaN
1 MX17004 2010 1 tmin d1 NaN
2 MX17004 2010 2 tmax d1 NaN
3 MX17004 2010 2 tmin d1 NaN
4 MX17004 2010 3 tmax d1 NaN
5 MX17004 2010 3 tmin d1 NaN
6 MX17004 2010 4 tmax d1 NaN
7 MX17004 2010 4 tmin d1 NaN
8 MX17004 2010 5 tmax d1 NaN
9 MX17004 2010 5 tmin d1 NaN

그럼에도 아직 불필요한 것들이 보이는 군요. 좀 더 깔끔한 데이터를 만들기 위해 tmin, tmax를 각각의 열로 만들겠습니다. 그리고 날짜 정보들을 합쳐서 date로 통합하겠습니다.

In [26]:
# Extracting day
df["day"] = df["day_raw"].str.extract("d(\d+)", expand=False)
df["id"] = "MX17004"

# To numeric values
df[["year", "month", "day"]] = df[["year", "month", "day"]].apply(
    lambda x: pd.to_numeric(x, errors="ignore")
)


# Creating a date from the different columns
def create_date_from_year_month_day(row):
    return datetime.datetime(year=row["year"], month=int(row["month"]), day=row["day"])


df["date"] = df.apply(lambda row: create_date_from_year_month_day(row), axis=1)
df = df.drop(["year", "month", "day", "day_raw"], axis=1)
df = df.dropna()

# Unmelting column "element"
df = df.pivot_table(index=["id", "date"], columns="element", values="value")
df.reset_index(drop=False, inplace=True)
df
Out[26]:
element id date tmax tmin
0 MX17004 2010-02-02 27.3 14.4
1 MX17004 2010-02-03 24.1 14.4
2 MX17004 2010-03-05 32.1 14.2

충분히 깔끔한 모양새가 되었습니다.

5. 하나의 관측 단위(observational units)가 여러 파일로 나누어져 있는 경우

Baby Names in Illinois

2014, 2015년도 미국 일리노이 주(Illinois)의 신생아의 (남자)이름을 수집한 데이터 입니다.

문제점:

  • 여러 표와 파일에 데이터가 흩어져 있다.
  • 연도(Year)”가 파일 이름에 적혀져 있다.

서로 다른 파일에 데이터가 흩어져 있어 조금 복잡한 과정이 필요합니다. 먼저 아래의 코드로 파일 리스트를 만들고 거기에서 연도 값을 뽑아냅니다. 그런 다음 각각의 파일에서 표를 만들어내고 마지막으로 concat기능으로 사용해 하나의 표로 합치겠습니다.

In [5]:
def extract_year(string):
    match = re.match(".+(\d{4})", string)
    if match is not None:
        return match.group(1)


path = "./data"
allFiles = glob.glob(path + "/201*-baby-names-illinois.csv")
frame = pd.DataFrame()
df_list = []
for file_ in allFiles:
    df = pd.read_csv(file_, index_col=None, header=0)
    df.columns = map(str.lower, df.columns)
    df["year"] = extract_year(file_)
    df_list.append(df)

df = pd.concat(df_list)
df.head(5)
Out[5]:
rank name frequency sex year
0 1 Noah 837 Male 2014
1 2 Alexander 747 Male 2014
2 3 William 687 Male 2014
3 4 Michael 680 Male 2014
4 5 Liam 670 Male 2014

마치며

이 글에서 가장 중점으로 둔것은 파이썬으로 지저분한 데이터를 깔끔하게 만드는 것이 었습니다. 그걸을 위해 Wickham의 논문에서 사용된 데이터를 살펴 보았죠. 깔끔한 데이터(Tidy data)의 최고의 장점은 시각화(Visualization)이 쉽다는 것에 있습니다. 그것은 다음에 다루어 보도록 하겠습니다.

앞으로는 Tidy data를 고려해서 데이터를 수집하도록 하세요. 모두의 시간은 소중하니까요.