본문 바로가기
Data Analysis

Tidy Data(깔끔한 데이터)

by rubyda 2021. 3. 18.
728x90

비정형데이터 과목을 수강하면서 교수님께서 Tidy Data를 언급하셨다. 데이터 분석을 하기 위해서는 Tidy Data 즉, 깔끔한 데이터가 필요하다. 이러한 깔끔한 데이터 형태를 만들기 위해서는 우리는 데이터 전처리, 변환등의 과정을 거치게 되는데 실제로 이 단계에서 정말 많은 시간이 소요된다. 그만큼 중요하다는 의미도 있다.

 

Tidy Data가 깔끔한? 정리된? 데이터라고는 알고있었지만, 좀 더 정확한 의미와 정의를 알아보고 싶어 찾아보았다. 찾아보면서 Tidy Data와 관련한 논문이 있어 그 논문을 리뷰하는 형태로 Tidy Data에 대해 정리하고자 한다.

 

리뷰해볼 논문은 Hadley Wickham의 논문이다.

 

   먼저, 논문에서는 말하고 있는 Tidy Data에 대해 한줄로 정의를 하자면 다음과 같다.  

 

"각 변수가 열이고 각 관측치가 행이 되도록 배열된 데이터이다."

 

위의 정의를 하나씩 풀어가며 Tidy Data에 대해 더 상세하게 알아보도록 하자.

 

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

1. 각 변수는 열을 구성합니다.

2. 각 관측치는 행을 구성합니다.

3. 각 관측단위는 표를 구성합니다.

 

https://cfss.uchicago.edu/notes/tidy-data/

 

논문에서는 다음과 같이 크게 3가지로 정의를 하고 있다. 위 그림을 보면서 이해하면 더 쉽게 이해할 수 있다.

 

지저분한 데이터란?

논문에서는 Tidy Data와 비교하여 지저분한 데이터도 같이 정의를 해주고 있다. 그 내용은 다음과 같다.

 

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

2. 같은 표에 다양한 관측 단위(observational units)가 있는 경우

3. 하나의 열(column)에 여러 값이 들어 있는 경우변수가 행과 열에 모두 포함되어 있는 경우

4. 변수가 행과 열에 모두 포함되어 있는 경우

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

 

논문에서 사용한 실제 데이터를 활용해서 직접 실습을 해보며 지저분한 데이터들을 처리하는 방법들에 대해 알아보도록 하자.


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

 

다음 데이터를 보면 Tidy Data라고 생각이 드는가?? 논문에 의하면 깔끔한 데이터가 아니다. 데이터 테이블의 열을 보면 소득의 범위 즉 열이름이 값의 범위로 들어가 있다. 이러한 형태로는 분석할때 불편함이 발생한다.

 

따라서 다음과 같은 데이터를 Tidy Data로 바꿔주는 작업이 필요하다. 여기서는 pandas에서 제공하는 melt함수를 사용해보도록 하겠다. melt는 녹인다는 의미로 컬럼들을 녹여서 행으로 보내겠다? 라는 의미로 생각하면 쉽다.

 

re_data = pd.melt(data1,["religion"], var_name="income", value_name="freq")
re_data = re_data.sort_values(by=["religion"]) 
re_data.head(10)

 

 

melt 함수 적용결과이다. 분석하기에 좋은 형태로 바뀌었다. 

 

다음 데이터도 살펴보자. 위 데이터는 빌도드차트 관련 데이터이다. 열을 보면 역시나 값으로 들어가 있다. 똑같이 melt함수를 사용해 보자.

 

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

# Formatting 
data2["week"] = pd.to_numeric(data2['week'].str.extract('(\d+)', expand=False))
data2["rank"] = pd.to_numeric(data2["rank"])

# Na 제거
data2 = data2.dropna()

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

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

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

data2.head(10)

참고한 데이터에서 week와 rank를 int형태로 변경하는 부분에서 Null값이 존재하여 에러가 발생하는 관계로 pd.to_numeric로 수정하였습니다.

 

다음과 같이 우리과 원하는 형태로 열이 바뀌었다. 하지만 여전이 데이터가 문제가 있어보인다.


(2) 같은 표에 다양한 관측 단위(observational units)가 있는 경우

바로 위의 표를 보면 track, time, genre의 많은 관측값들이 중복되어 들어가 있다. 논문에서는 다음과 같은 데이터를 노래, 랭킹으로 구분하였다. 실습을 한번 해보도록 하자.

 

song_table

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)

rank_table

ranks = pd.merge(billboard, songs, on=["year","artist.inverted", "track", "time", "genre"])
ranks = ranks[["song_id", "date","rank"]]
ranks.head(10)

다음과 같이 두개의 테이블로 나누었다. 중복된 데이터가 있어 한눈에 파악하기 어려웠던 부분들을 다음과 같이 두개의 카테고리로 분류해 보니 훨씬 Tidy해 보인다.

 

여기서 중요한점은 song_id를 만들어야 한다는 점이다. 즉, 공통으로 묶을 수 있는 키를 만들어주는 것이다.

데이터를 조인할 경우를 대비하여!!


(3) 하나의 열(column)에 여러 값이 들어 있는 경우

다음 데이터는 결핵환자 데이터이다. 열을 보면 여러값들이 들거가 있는데 각각의 의미는 다음과 같다.

 

- 성별:("m" 또는 "f")
- 연령 그룹: ("0-14", 15-24", "25-34", "45-54", "55-64", "65", "알 수 없음")
- 결측치의 종류가 0과 결측값("NaN")로 혼합되어 있다.

 

다음 데이터도 논문에 의하면 Tidy하지 못한 데이터이다. 왜일까?? 우선 열을 보면 많은 값들이 존재한다. 괜찮다고 생각할수도 있지만 그 변수들의 의미를 생각해보면 그렇지 않다. 각 열에서 앞부분은 성별, 뒤에 숫자는 연령대를 의미한다. 

 

이렇게 서로 관계가 있고, 어떠한 구간으로 존재하기 때문에 이러한 데이터는 열에 있기 보다는 앞의 예제처럼 행으로 보내주는 작업이 필요하다.

 

df = pd.melt(data3, id_vars=["country","year"], value_name="cases", var_name="sex_and_age")
df.head(10)

melt 함수로 다음과 같이 변경을 하였다. 여기서 더 tidy 형태로 바꿔보자. 좀 더 분석기 좋은 형태로!!

 

그렇기 위해서는 sex_and_age 변수에서 sex와 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)

훨씬 Tidy한 데이터가 되었다.


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

 

다음 데이터는 멕시코 기상청에서 측정한 기상 데이터터이다.

 

다음 데이터는 Tidy한 데이터인가?? 아니다. 앞에서 해왔듯이 이제 d1~d8데이터는 행으로 바꿔주는 편이 좋겠다는 사실을 쉽게 파악할 수 있다. 한번 바꿔보자.

data4 = pd.melt(data4, id_vars=["id", "year","month","element"], var_name="day_raw")
data4.head(10)

이젠 Tidy한가?? 아직 부족하다. element 변수를 보면 각각의 id당 element의 값이 tmax, tmin 두개의 값을 가진다. id, year, month은 같지만 element만 다른 값으로 존재한다. 이러한 경우는 행보다는 열에 어울리기 때문에 열로 바꿔줘야한다. 

 

무슨말인지 잘 모르겠으면 우선 바꾸고 비교를 해보자.

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

# To numeric values
data4[["year","month","day"]] = data4[["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"])

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

# Unmelting column "element"
data4 = data4.pivot_table(index=["id","date"], columns="element", values="value")
data4.reset_index(drop=False, inplace=True)
data4

어떠한가?? 위에 데이터보다 더 Tidy하다. 더 Tidy하다라는 의미가 어려우면 분석을 진행했을때 어떠한 형태가 더 편리할까?로 바꿔서 생각해 보면 좀 더 쉽게 느껴지는것 같다.

 

여기서는 추가적으로 날짜 데이터도 합쳐주는 작업을 했는데 이 부분은 분석 방법에 따라 바뀔 수 있다고 생각한다.


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

미국의 신생아가들의 이름이 담긴 데이터이다.

 

다음 데이터는 2014, 2015 데이터가 각자 다른 파일로 존재한다. 분석하기 위해서는 하나로 합쳐주는 작업이 필요하다. 분석을 할때 이러한 상황은 많이 발생한다. 

 

정리를 하자면 서로 다른 파일에 존재하는 데이터들을 합쳐주는 작업이 필요하다는 것이다.

def extract_year(string):
    match = re.match(".+(\d{4})", string) 
    if match != None: return match.group(1)
    
path = './' # 각자 데이터가 담긴 경로
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)

 

 

 

Tidy Data의 의미는 사람보다 조금씩 차이가 존재할 수 있지만, 논문에 담긴 내용들은 기본적으로 포함하고 있어야 되는 부분인것 같다. Tidy 형태가 중요한 이유는 분석할때 편하기 때문이다.

 

앞으로 데이터 분석을 할때 항상 이 데이터가 Tidy한 상태인가?? 조금 더 Tidy형태로 만들수는 없을까?? 하는 생각을 계속 하면서 습관이 되도록 해야겠다.


 

참고)

cfss.uchicago.edu/notes/tidy-data/

 

Tidy data | Computing for the Social Sciences

library(tidyverse) Most data analysts and statisticians analyze data in a spreadsheet or tabular format. This is not the only way to store information,1 however in the social sciences it has been the paradigm for many decades. Tidy data is a specific way o

cfss.uchicago.edu

github.com/nickhould/tidy-data-python

 

nickhould/tidy-data-python

Tidy Data in Python Jupyter Notebook. Contribute to nickhould/tidy-data-python development by creating an account on GitHub.

github.com