我最近发现了一篇Hidley Wickham在2014年发布的名为Tidy Data的论文,文中重点关注数据清理,数据整洁的一个方面:构建数据集以便于分析。 通过论文,Wickham演示了在分析之前如何以标准化方式构建任何数据集。 他详细介绍了不同类型的数据集以及如何将它们标准化。
作为一名数据科学家,我认为你应该非常熟悉这种数据集的标准化结构。 数据清理是数据科学中最常见的任务之一。 无论你正在处理何种数据或执行何种分析,你都必须在某个时刻清理数据。以标准格式整理数据可以使事情变得更轻松。你可以在不同的分析中重复使用一组标准工具。
在这篇文章中,我将总结Wickham在他的论文中使用的一些数据整洁的例子,我将演示如何使用Python pandas库进行操作。
定义整洁数据
Wickham定义为整洁的结构具有以下属性:
- 每个变量(Variable)形成一列并有值(Value)
- 每个观察结果(Observation)形成一行
- 每种类型的观察单元形成一个表格
一些定义:
- 变量(Variable):度量或属性。身高,体重,性别等
- 值(Value):实际测量值或属性。152厘米,80公斤,女性等
- 观察结果(Observation):所有在同一单元上测量的值。每个人
脏数据集例子:
Treatment A | Treatment B | |
---|---|---|
John Smith | - | 2 |
Jane Doe | 16 | 11 |
Mary Johnson | 3 | 1 |
整洁数据集例子:
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的论文中摘取的以下示例,我们将把凌乱的数据集变成整洁的格式。这里的目的不是分析数据集,而是在分析之前以标准化方式准备它们。以下是我们要解决的五种凌乱的数据集:
- 列标题是值,而不是变量名。
- 多种变量存储在一列中。
- 变量同时存储在行和列中。
- 多种类型的观察单元存储在同一个表中。
- 单个观察单元存储在多个表中。
注意:本文中提供的所有代码均可在Github上获得。
列标题是值,而不是变量名
Pew研究中心数据集
该数据集探讨了收入与宗教之间的关系。
问题:列标题由可能的收入值组成。
import pandas as pd
import datetime
from os import listdir
from os.path import isfile, join
import glob
import re
df = pd.read_csv("./data/pew-raw.csv")
df
religion | <$10k | $10-20k | $20-30k | $30-40k | $40-50k | $50-75k |
---|---|---|---|---|---|---|
Agnostic | 27 | 34 | 60 | 81 | 76 | 137 |
Atheist | 12 | 27 | 37 | 52 | 35 | 70 |
Buddhist | 27 | 21 | 30 | 34 | 33 | 58 |
Catholic | 418 | 617 | 732 | 670 | 638 | 1116 |
Dont know/refused | 15 | 14 | 15 | 11 | 10 | 35 |
Evangelical Prot | 575 | 869 | 1064 | 982 | 881 | 1486 |
Hindu | 1 | 9 | 7 | 9 | 11 | 34 |
Historically Black Prot | 228 | 244 | 236 | 238 | 197 | 223 |
Jehovahs Witness | 20 | 27 | 24 | 24 | 21 | 30 |
Jewish | 19 | 19 | 25 | 25 | 30 | 95 |
此数据集中收入值不应该是列标题,而应该是income
列里的值。为了整理这个数据集,我们需要整合(melt)它。 pandas库有一个内置函数,可以做到这一点。它将DataFrame从宽格式“展开”到长格式。后面我们也会重复使用到这个函数。
formatted_df = pd.melt(df,
["religion"],
var_name="income",
value_name="freq")
formatted_df = formatted_df.sort_values(by=["religion"])
formatted_df.head(10)
这将输出整洁的数据集:
religion | income | freq |
---|---|---|
Agnostic | <$10k | 27 |
Agnostic | $30-40k | 81 |
Agnostic | $40-50k | 76 |
Agnostic | $50-75k | 137 |
Agnostic | $10-20k | 34 |
Agnostic | $20-30k | 60 |
Atheist | $40-50k | 35 |
Atheist | $20-30k | 37 |
Atheist | $10-20k | 27 |
Atheist | $30-40k | 52 |
Billboard Top 100数据集
此数据集包含Billboard周排名歌曲从进入前100开始到随后75周期间的变化。
问题:
- 列标题由值组成:周数(
x1st.week
,...) - 如果歌曲在前100名中不到75周,则其余列将填充缺失值。
df = pd.read_csv("./data/billboard.csv", encoding="mac_latin2")
df.head(10)
year | artist.inverted | track | time | genre | date.entered | date.peaked | x1st.week | x2nd.week | ... |
---|---|---|---|---|---|---|---|---|---|
2000 | Destiny's Child | Independent Women Part I | 3:38 | Rock | 2000-09-23 | 2000-11-18 | 78 | 63.0 | ... |
2000 | Santana | Maria, Maria | 4:18 | Rock | 2000-02-12 | 2000-04-08 | 15 | 8.0 | ... |
2000 | Savage Garden | I Knew I Loved You | 4:07 | Rock | 1999-10-23 | 2000-01-29 | 71 | 48.0 | ... |
2000 | Madonna | Music | 3:45 | Rock | 2000-08-12 | 2000-09-16 | 41 | 23.0 | ... |
2000 | Aguilera, Christina | Come On Over Baby (All I Want Is You) | 3:38 | Rock | 2000-08-05 | 2000-10-14 | 57 | 47.0 | ... |
2000 | Janet | Doesn't Really Matter | 4:17 | Rock | 2000-06-17 | 2000-08-26 | 59 | 52.0 | ... |
2000 | Destiny's Child | Say My Name | 4:31 | Rock | 1999-12-25 | 2000-03-18 | 83 | 83.0 | ... |
2000 | Iglesias, Enrique | Be With You | 3:36 | Latin | 2000-04-01 | 2000-06-24 | 63 | 45.0 | ... |
2000 | Sisqo | Incomplete | 3:52 | Rock | 2000-06-24 | 2000-08-12 | 77 | 66.0 | ... |
2000 | Lonestar | Amazed | 4:25 | Country | 1999-06-05 | 2000-03-04 | 81 | 54.0 | ... |
此数据集的整洁版本是周数没有作为列而是作为单个列的值。为此,我们将周数整合到单个列。我们将为每条记录每周创建一行。如果给定周没有数据,我们将不会为它创建一行。
# 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)
df["rank"] = df["rank"].astype(int)
# Cleaning out unnecessary rows
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)
整洁的数据集如下所示。歌曲详情仍有很多重复记录:曲目名称,时间和流派。因此,根据Wickham的定义,这个数据集仍然不够整洁。我们将在下一个例子中解决这个问题。
year | artist.inverted | track | time | genre | week | rank | date |
---|---|---|---|---|---|---|---|
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 1 | 87 | 2000-02-26 |
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 2 | 82 | 2000-03-04 |
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 3 | 72 | 2000-03-11 |
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 4 | 77 | 2000-03-18 |
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 5 | 87 | 2000-03-25 |
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 6 | 94 | 2000-04-01 |
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 7 | 99 | 2000-04-08 |
2000 | 2Ge+her | The Hardest Part Of Breaking Up (Is Getting Ba... | 3:15 | R&B | 1 | 91 | 2000-09-02 |
2000 | 2Ge+her | The Hardest Part Of Breaking Up (Is Getting Ba... | 3:15 | R&B | 2 | 87 | 2000-09-09 |
2000 | 2Ge+her | The Hardest Part Of Breaking Up (Is Getting Ba... | 3:15 | R&B | 3 | 92 | 2000-09-16 |
单表多类型
继续使用Billboard数据集,我们现在将解决前面提到的重复问题。
问题:
- 单个表中有多个观察单元(
song
及其rank
)
我们首先将创建一个表songs
,其中包含每首歌曲的详细信息:
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)
year | artist.inverted | track | time | genre | song_id |
---|---|---|---|---|---|
2000 | 2 Pac | Baby Don't Cry (Keep Ya Head Up II) | 4:22 | Rap | 0 |
2000 | 2Ge+her | The Hardest Part Of Breaking Up (Is Getting Ba... | 3:15 | R&B | 1 |
2000 | 3 Doors Down | Kryptonite | 3:53 | Rock | 2 |
2000 | 3 Doors Down | Loser | 4:24 | Rock | 3 |
2000 | 504 Boyz | Wobble Wobble | 3:35 | Rap | 4 |
2000 | 98� | Give Me Just One Night (Una Noche) | 3:24 | Rock | 5 |
2000 | A*Teens | Dancing Queen | 3:44 | Pop | 6 |
2000 | Aaliyah | I Don't Wanna | 4:15 | Rock | 7 |
2000 | Aaliyah | Try Again | 4:03 | Rock | 8 |
2000 | Adams, Yolanda | Open My Heart | 5:30 | Gospel | 9 |
然后我们将创建一个表ranks
,仅包含song_id
,date
和rank
。
ranks = pd.merge(billboard, songs, on=["year","artist.inverted", "track", "time", "genre"])
ranks = ranks[["song_id", "date","rank"]]
ranks.head(10)
song_id | date | rank |
---|---|---|
0 | 2000-02-26 | 87 |
0 | 2000-03-04 | 82 |
0 | 2000-03-11 | 72 |
0 | 2000-03-18 | 77 |
0 | 2000-03-25 | 87 |
0 | 2000-04-01 | 94 |
0 | 2000-04-08 | 99 |
1 | 2000-09-02 | 91 |
1 | 2000-09-09 | 87 |
1 | 2000-09-16 | 92 |
多个变量存在同一列
世界卫生组织的结核病记录
该数据集按国家,年份,年龄和性别记录确诊的结核病病例数。
问题:
- 有些列包含多个值:性别和年龄。
- 零和缺失值
NaN
的混合。 这是数据收集过程产生的,而区分数据对此数据集很重要。
df = pd.read_csv("./data/tb-raw.csv")
df
country | year | m014 | m1524 | m2534 | m3544 | m4554 | m5564 | m65 | mu | f014 |
---|---|---|---|---|---|---|---|---|---|---|
AD | 2000 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | NaN | NaN |
AE | 2000 | 2 | 4 | 4 | 6 | 5 | 12 | 10 | NaN | 3 |
AF | 2000 | 52 | 228 | 183 | 149 | 129 | 94 | 80 | NaN | 93 |
AG | 2000 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | NaN | 1 |
AL | 2000 | 2 | 19 | 21 | 14 | 24 | 19 | 16 | NaN | 3 |
AM | 2000 | 2 | 152 | 130 | 131 | 63 | 26 | 21 | NaN | 1 |
AN | 2000 | 0 | 0 | 1 | 2 | 0 | 0 | 0 | NaN | 0 |
AO | 2000 | 186 | 999 | 1003 | 912 | 482 | 312 | 194 | NaN | 247 |
AR | 2000 | 97 | 278 | 594 | 402 | 419 | 368 | 330 | NaN | 121 |
AS | 2000 | NaN | NaN | NaN | NaN | 1 | 1 | NaN | NaN | NaN |
为了整理这个数据集,我们需要从标题中删除不同的值并将它们展开成行。 我们首先需要将sex + age group
列整合为单列。整合后,我们将从中派生出三列:sex
,age_lower
和age_upper
。有了这些,我们就能够正确地构建一个整洁的数据集。
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})")
# 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(ascending=True,columns=["country", "year", "sex", "age"])
df.head(10)
执行后得出整洁的数据集。
country | year | cases | sex | age |
---|---|---|---|---|
AD | 2000 | 0 | m | 0-14 |
AD | 2000 | 0 | m | 15-24 |
AD | 2000 | 1 | m | 25-34 |
AD | 2000 | 0 | m | 35-44 |
AD | 2000 | 0 | m | 45-54 |
AD | 2000 | 0 | m | 55-64 |
AE | 2000 | 3 | f | 0-14 |
AE | 2000 | 2 | m | 0-14 |
AE | 2000 | 4 | m | 15-24 |
AE | 2000 | 4 | m | 25-34 |
变量同时在行和列
全球历史气候网络数据集
该数据集包含2010年墨西哥气象站(MX17004)五个月的每日天气记录。
问题:
- 变量存储在行(
tmin
,tmax
)和列(days
)中。
df = pd.read_csv("./data/weather-raw.csv")
df
id | year | month | element | d1 | d2 | d3 | d4 | d5 | d6 | d7 | d8 |
---|---|---|---|---|---|---|---|---|---|---|---|
MX17004 | 2010 | 1 | tmax | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
MX17004 | 2010 | 1 | tmin | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
MX17004 | 2010 | 2 | tmax | NaN | 27.3 | 24.1 | NaN | NaN | NaN | NaN | NaN |
MX17004 | 2010 | 2 | tmin | NaN | 14.4 | 14.4 | NaN | NaN | NaN | NaN | NaN |
MX17004 | 2010 | 3 | tmax | NaN | NaN | NaN | NaN | 32.1 | NaN | NaN | NaN |
MX17004 | 2010 | 3 | tmin | NaN | NaN | NaN | NaN | 14.2 | NaN | NaN | NaN |
MX17004 | 2010 | 4 | tmax | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
MX17004 | 2010 | 4 | tmin | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
MX17004 | 2010 | 5 | tmax | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
MX17004 | 2010 | 5 | tmin | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
为了使数据集整洁,我们需要将三个错位变量(tmin
,tmax
和days
)作为三个单独的列: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
id | date | tmax | tmin |
---|---|---|---|
MX17004 | 2010-02-02 | 27.3 | 14.4 |
MX17004 | 2010-02-03 | 24.1 | 14.4 |
MX17004 | 2010-03-05 | 32.1 | 14.2 |
同个观察对象存于多表
数据集:2014/2015年伊利诺伊州男婴姓名。
问题:
- 数据分布在多个表/文件中。
- “Year”变量出现在文件名中。
为了将这些不同的文件加载到单个DataFrame中,我们可以运行自定义脚本将文件附加到一起。此外,我们还需要从文件名中提取“Year”变量。
def extract_year(string):
match = re.match(".+(\d{4})", string)
if match != 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)
rank | name | frequency | sex | year |
---|---|---|---|---|
1 | Noah | 837 | Male | 2014 |
2 | Alexander | 747 | Male | 2014 |
3 | William | 687 | Male | 2014 |
4 | Michael | 680 | Male | 2014 |
5 | Liam | 670 | Male | 2014 |
总结
在这篇文章中,我专注于Wickham论文的数据操作部分。我的主要目的是在Python中演示数据操作。值得一提的是,他的论文中有一个很重要的部分提到了通过整理数据集可以从中受益的工具和可视化。在这篇文章中我没有涵盖此部分内容。
总的来说,我乐于准备这篇文章并将数据集整理成简洁的格式。这种格式使查询和过滤数据变得更加容易。这种方法可以在分析中更轻松地重用库和代码。它还可以更轻松地与其他数据分析人员共享数据集。
原文链接:Tidy Data in Python
网友评论