깔끔한 데이터(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
더 알아 보기¶
깔끔한 데이터(Tidy data)의 특징¶
Jeff Leek가 쓴 책 The Elements of Data Analytic Style에서 정의한 깔끔한 데이터는 아래와 같은 특징을 가집니다.
- 각 변수는 개별의 열(column)으로 존재한다.
- 각 관측치는 행(row)를 구성한다.
- 각 표는 단 하나의 관측기준에 의해서 조직된 데이터를 저장한다.
- 만약 여러개의 표가 존재한다면, 적어도 하나이상의 열(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의 논문에 있던 데이터를 가지고 아래의 문제를 해결해 보도록 하겠습니다. 여기서 우리의 목표는 데이터 분석이 아니고 깔끔한 데이터를 만드는 것임을 명심하세요.
지저분한 데이터의 일반적인 모습은 다음과 같습니다.
- 열 이름(Column header)이 변수 이름이 아니고 값인 경우
- 같은 표에 다양한 관측 단위(observational units)가 있는 경우
- 하나의 열(column)에 여러 값이 들어 있는 경우
- 변수가 행과 열에 모두 포함되어 있는 경우
- 하나의 관측 단위(observational units)가 여러 파일로 나누어져 있는 경우
우리는
Python
의 대표적인 라이브러리인Pandas
를 사용하도록 할게요.
# 필요한 라이브러리 불러들이기
import pandas as pd
import datetime
import glob
import re # 정규식
1. 열 이름(Column header)이 변수 이름이 아니고 값인 경우¶
Pew Research Center Dataset¶
종교에 따른 개인의 수입의 관한 데이터입니다. 먼저 pandas
의 read_csv
기능을 사용해 파일을 읽어옵니다.
df = pd.read_csv("./data/pew-raw.csv")
df
- Evangelical Prot는 기독교 종파중에 하나로 '개신교'입니다.
문제점: 이 데이터들의 문제는 열 이름(columns headers)이 개인소득의 범위로 되어 있다는 것이죠.
다시 말해서 보기에는 좋아보일지는 몰라도 분석하기에는 어려운 형식입니다.
이러한 데이터를 Tidy data 형태로 변환하기 위해서 pandas
라이브러리에서는 아주 쉬운 기능을 제공해 줍니다.
바로 melt
라는 기능이지요. pivot table의 반대되는 개념으로 행이 많은 데이터를 열이 많은 데이터로 바꿔줍니다.
melt
는 아주 유용하기 때문에 앞으로도 자주 언급 됩니다.
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개만 보겠습니다.
이것이 Pew Research Center Dataset 의 Tidy data 형태 입니다.
Billboard Top 100 Dataset¶
이 데이터는 아주 오래전 같은 1999년부터 2000년까지의 빌보드차트 주간 순위 변동을 포함하고 있는 데이터 입니다.
df = pd.read_csv("./data/billboard.csv", encoding="mac_latin2")
df.head(10)
문제점:
- 열 이름(columns headers)이 값으로 구성되어 있다:
x1st.week
,x2st.week
등등 - 순위가 100위권 밖으로 밀려나게 되면
Nan
값을 가지고, 따라서 필요없는 부수적인 값이 많아진다.
위 데이터의 깔끔한 데이터를 만들기 위해서는 다시 melt
기능을 사용하겠습니다. 각각의 열이 행이 되도록 하고, 순위가 100위 밖으로 밀려난 경우(Nan
값을 갖는경우)에는 행을 삭제할게요.
# 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)
위와 같이 데이터를 깔끔하게 만들었습니다. 하지만 track, time, genre 열을 보시면 아주 많은 중복이 있는 것을 알 수 있습니다. 이러한 점을 해결 하는 방법은 다음 예제에서 다루어 보겠습니다.
2. 하나의 표에 여러가지 타입¶
위에서 다루었던 빌보드차트 데이터를 가지고 데이터가 반복되는 문제를 해결해 보겠습니다.
묹제점:
- 다양한 관측 단위(observational units), 여기서는
song
과rank
가 하나의 표에 들어 있습니다. 이를 위해서는 표를 나눌 필요가 있습니다.
먼저, 각각의 노래의 자세한 내용을 담고 있는 표를 만들어 보겠습니다. 그런다음 각각의 song_id
를 부여합니다. 그런다음 순위 값을 가지고 있는 표를 song_id
로 정리합니다.
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)
위 와같은 새로운 표를 분리하고, 아래와 같이 순위를 포함하고 있는 표를 새로 만들어줍니다.
두개의 표를 연결하기 위해
song_id
열을 만드는 것을 주의하세요
ranks = pd.merge(
billboard, songs, on=["year", "artist.inverted", "track", "time", "genre"]
)
ranks = ranks[["song_id", "date", "rank"]]
ranks.head(10)
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")를 나타냅니다.
df = pd.read_csv("./data/tb-raw.csv")
df
이 데이터를 정리하기 위해서는 먼저 melt
를 이용해 sex
+ age group
를 합쳐서 하나의 행으로 만들겠습니다.
그런 다음에 다시 행을 sex
, age
로 구분해서 정리하도록 하죠.
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)
이것이 정리된 결과 입니다.
4. 변수가 행과 열에(rows and columns) 모두 포함되어 있는 경우¶
Global Historical Climatology Network Dataset¶
이것은 2010년도 멕시코 기상청(MX17004)에서 5개월 동안 측정한 기상 데이터입니다.
df = pd.read_csv("./data/weather-raw.csv")
문제점:
- 변수들이 행(
tmin
,tmax
) 과 열(days
)에 존재합니다.
깔끔한 데이터를 만들기 위해 melt
기능을 사용해 day_raw
를 각각의 열로 만들겠습니다.
df = pd.melt(df, id_vars=["id", "year", "month", "element"], var_name="day_raw")
df.head(10)
그럼에도 아직 불필요한 것들이 보이는 군요. 좀 더 깔끔한 데이터를 만들기 위해 tmin
, tmax
를 각각의 열로 만들겠습니다. 그리고 날짜 정보들을 합쳐서 date
로 통합하겠습니다.
# 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
충분히 깔끔한 모양새가 되었습니다.
5. 하나의 관측 단위(observational units)가 여러 파일로 나누어져 있는 경우¶
Baby Names in Illinois¶
2014, 2015년도 미국 일리노이 주(Illinois)의 신생아의 (남자)이름을 수집한 데이터 입니다.
문제점:
- 여러 표와 파일에 데이터가 흩어져 있다.
- 연도(Year)”가 파일 이름에 적혀져 있다.
서로 다른 파일에 데이터가 흩어져 있어 조금 복잡한 과정이 필요합니다.
먼저 아래의 코드로 파일 리스트를 만들고 거기에서 연도 값을 뽑아냅니다. 그런 다음 각각의 파일에서 표를 만들어내고 마지막으로 concat
기능으로 사용해 하나의 표로 합치겠습니다.
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)
마치며¶
이 글에서 가장 중점으로 둔것은 파이썬으로 지저분한 데이터를 깔끔하게 만드는 것이 었습니다. 그걸을 위해 Wickham의 논문에서 사용된 데이터를 살펴 보았죠. 깔끔한 데이터(Tidy data)의 최고의 장점은 시각화(Visualization)이 쉽다는 것에 있습니다. 그것은 다음에 다루어 보도록 하겠습니다.
앞으로는 Tidy data를 고려해서 데이터를 수집하도록 하세요. 모두의 시간은 소중하니까요.