数据分析和建模的大量编程工作都是在数据准备上的:加载、清理、转换以及重塑。
pandas和Python标准库提供了一组高级的、灵活的、高效的核心函数和算法,
能够轻松地将数据规整化为正确的形式。
尤其,Pandas 的许多功能都来自实际应用中的需求。
数据规整主要包括:合并数据集、重塑和轴向旋转、数据转换、字符串操作。
合并数据集
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
import json
pandas提供了一些内置的方式来处理数据的合并:
- pandas.merge(): 根据一个或者多个键值,将不同 DataFrame 中的行连接起来,就是SQL中的数据库连接工作。
- pandas.concat(): 沿着一条轴将多个对象堆叠在一起
- DataFrame.combine_first(): 将重复数据编接在一起,用一个对象中的值填充另一个对象中的缺失值。
数据库风格的DataFrame合并¶
数据集的合并(merge)或者连接(join)运算,是通过一个或者多个键将行链接起来。这是关系型数据库的核心。
在合并(merge)时,支持内连接(inner)、左连接(left)、右连接(right)、外连接(outer),通过how
指定。默认为内连接(inner)。
多对多的合并,结果是行的笛卡尔积,即针对一个键值,两个对象对应值的所有组合。连接方式只影响出现在结果中的键。其中:
- 内连接只保留合并列中的交集
- 左连接?
- 右连接?
- 外连接保留和并列的并集,相当于组合了左连接和右连接的结果
df1 = DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
'data1': range(7)})
df1
df2 = DataFrame({'key': ['a', 'b', 'd'],
'data2': range(3)})
df2
# 默认用重复的列名进行合并,并且只保留合并列中的交集,其他舍去(内连接)
pd.merge(df1, df2)
# 最好显示指定合并列
pd.merge(df1, df2, on='key')
# 列名不同时,可以分别指定
df3 = DataFrame({'lkey': ['b', 'b', 'a', 'c', 'a', 'a', 'b'],
'data1': range(7)})
df4 = DataFrame({'rkey': ['a', 'b', 'd'],
'data2': range(3)})
pd.merge(df3, df4, left_on='lkey', right_on='rkey')
# 外连接:
pd.merge(df1,df2,how = 'outer')
# 左连接
df1 = DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
'data1': range(6)})
df1
df2 = DataFrame({'key': ['a', 'b', 'a', 'b', 'd'],
'data2': range(5)})
df2
# 左连接
pd.merge(df1, df2, on='key', how='left')
#对多个键进行合并,传入一个由列名组成的列表即可
left = DataFrame({'key1': ['foo', 'foo', 'bar'],
'key2': ['one', 'two', 'one'],
'lval': [1, 2, 3]})
right = DataFrame({'key1': ['foo', 'foo', 'bar', 'bar'],
'key2': ['one', 'one', 'one', 'two'],
'rval': [4, 5, 6, 7]})
pd.merge(left, right, on=['key1', 'key2'], how='outer')
# 对于重复列名,pandas会自动添加后缀
pd.merge(left,right,on = 'key1')
# 可以通过suffixes选项指定后缀
pd.merge(left, right, on='key1', suffixes=('_left', '_right'))
索引上的合并¶
如果连接键在索引中,可以通过 left_index = True或者right_index = True 指定。
left1 = DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'],
'value': range(6)})
left1
right1 = DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])
right1
pd.merge(left1, right1, left_on='key', right_index=True)
pd.merge(left1, right1, left_on='key', right_index=True, how='outer')
# 对于层次化索引
lefth = DataFrame({'key1': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
'key2': [2000, 2001, 2002, 2001, 2002],
'data': np.arange(5.)})
lefth
righth = DataFrame(np.arange(12).reshape((6, 2)),
index=[['Nevada', 'Nevada', 'Ohio', 'Ohio', 'Ohio', 'Ohio'],
[2001, 2000, 2000, 2000, 2001, 2002]],
columns=['event1', 'event2'])
righth
#这种情况下,必须指明用作合并键的多个列(注意对重复索引值的处理)
#注意得到的结果的index是跟左边对象的index一致
pd.merge(lefth, righth, left_on=['key1', 'key2'], right_index=True)
# 同时使用合并双方的索引
left2 = DataFrame([[1., 2.], [3., 4.], [5., 6.]], index=['a', 'c', 'e'],
columns=['Ohio', 'Nevada'])
right2 = DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]],
index=['b', 'c', 'd', 'e'], columns=['Missouri', 'Alabama'])
pd.merge(left2, right2, how='outer', left_index=True, right_index=True)
# DataFrame.join(), 能更方便地实现按索引合并
# 还可以用作合并多个带有相同或者相似索引的DataFrame对象,而不管有没有重叠的列
# DataFrame的join方法是在连接键上做左连接
left2.join(right2, how='outer')
left1.join(right1, on='key')
# join()方法支持参数DataFrame的索引跟调用者DataFrame的某个列之间的连接(这个方法有点像merge中的left_index这样的参数)
another = DataFrame([[7., 8.], [9., 10.], [11., 12.], [16., 17.]],
index=['a', 'c', 'e', 'f'], columns=['New York', 'Oregon'])
another
left2.join([right2, another])
# 对于简单的索引合并,还可以向join传入多个DataFrame
left2.join([right2, another], how='outer')
缺失值的合并¶
a = Series([np.nan, 2.5, np.nan, 3.5, 4.5, np.nan],
index=['f', 'e', 'd', 'c', 'b', 'a'])
a
b = Series(np.arange(len(a), dtype=np.float64),
index=['f', 'e', 'd', 'c', 'b', 'a'])
b[-1] = np.nan
b
np.where(pd.isnull(a), b, a)
b[:-2].combine_first(a[2:])
df1 = DataFrame({'a': [1., np.nan, 5., np.nan],
'b': [np.nan, 2., np.nan, 6.],
'c': range(2, 18, 4)})
df2 = DataFrame({'a': [5., 4., np.nan, 3., 7.],
'b': [np.nan, 3., 4., 6., 8.]})
df1.combine_first(df2)
重塑和轴向旋转
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
import json
data = DataFrame(np.arange(6).reshape((2, 3)),
index=pd.Index(['Ohio', 'Colorado'], name='state'),
columns=pd.Index(['one', 'two', 'three'], name='number'))
data
result = data.stack()
result
result.unstack()
# 默认情况下,unstack处理的是内层的索引,若想别的层次,传入编号或者名称即可,注意最外一层编号为0
result.unstack(0)
# 也可用列名指定
result.unstack('state')
# 下面看有缺失值的情况,unstack()会标示出缺失值
s1 = Series([0, 1, 2, 3], index=['a', 'b', 'c', 'd'])
s2 = Series([4, 5, 6], index=['c', 'd', 'e'])
data2 = pd.concat([s1, s2], keys=['one', 'two'])
data2.unstack()
# stack会滤除缺失数据
data2.unstack().stack()
# 保留缺失值
data2.unstack().stack(dropna=False)
# 对DataFrame进行unstack时,作为旋转轴的级别成为结果中最低的,弄到最内层
df = DataFrame({'left': result, 'right': result + 5},
columns=pd.Index(['left', 'right'], name='side'))
df
df.unstack('state')
df.unstack('state').stack('side')
pivot: 将“长格式”转换为“宽格式”¶
data = pd.read_csv('data/ch07/macrodata.csv')
periods = pd.PeriodIndex(year=data.year, quarter=data.quarter, name='date')
data = DataFrame(data.to_records(),
columns=pd.Index(['realgdp', 'infl', 'unemp'], name='item'),
index=periods.to_timestamp('D', 'end'))
ldata = data.stack().reset_index().rename(columns={0: 'value'})
ldata[:10]
# 将data、item作为行、列名,value填充进二维表
pivoted = ldata.pivot('date', 'item', 'value')
pivoted.head()
ldata['value2'] = np.random.randn(len(ldata))
ldata[:10]
pivoted = ldata.pivot('date', 'item')
pivoted[:5]
pivoted['value'][:5]
# pivot其实只是一个“快捷方式而已”, 其本质是用set_index创建层次化索引,再用unstack重塑
unstacked = ldata.set_index(['date', 'item']).unstack('item')
unstacked[:7]
数据转换
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
import json
重排之后,下面介绍数据的过滤、清理、以及其他转换工作。
去除重复数据¶
data = DataFrame({'k1': ['one'] * 3 + ['two'] * 4,
'k2': [1, 1, 2, 3, 3, 4, 4]})
data
data.duplicated()
# 得到去重之后的DataFrame,这是非常常用的
data.drop_duplicates()
# 可以选定需要去重的列
data['v1'] = range(7)
data.drop_duplicates(['k1'])
# 默认保留第一次出现的行, 可以设定为最后一个
data.drop_duplicates(['k1', 'k2'], keep='last')
用函数或者映射(mapping)进行数据转换¶
可以实现根据值进行转换。
data = DataFrame({'food': ['bacon', 'pulled pork', 'bacon', 'Pastrami',
'corned beef', 'Bacon', 'pastrami', 'honey ham',
'nova lox'],
'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
data
# 添加一个肉类到动物的映射
meat_to_animal = {
'bacon': 'pig',
'pulled pork': 'pig',
'pastrami': 'cow',
'corned beef': 'cow',
'honey ham': 'pig',
'nova lox': 'salmon'
}
# Series的map方法可以接受一个函数或含有映射关系的字典型对象
# 为了保证映射正确,先转换大小写。 这种方法很常用
data['animal'] = data['food'].map(str.lower).map(meat_to_animal)
data
# 也可以用一个函数实现上述功能
data['food'].map(lambda x: meat_to_animal[x.lower()])
替换值¶
使用 replace()
函数,可以更简单的实现值替换。
data = Series([1., -999., 2., -999., -1000., 3.])
data
data.replace(-999, np.nan)
data.replace([-999, -1000], np.nan)
data.replace([-999, -1000], [np.nan, 0])
data.replace({-999: np.nan, -1000: 0})
重命名轴索引¶
与值一样,轴标签页可以用 map()
函数进行映射。
data = DataFrame(np.arange(12).reshape((3, 4)),
index=['Ohio', 'Colorado', 'New York'],
columns=['one', 'two', 'three', 'four'])
data.index.map(str.upper)
data.index = data.index.map(str.upper)
data
# 对于轴,与 replace()类似的函数是 rename()
data.rename(index=str.title, columns=str.upper)
data.rename(index={'OHIO': 'INDIANA'},
columns={'three': 'peekaboo'})
# inplace 指定 就地修改,而无需新建一个数据结构
_ = data.rename(index={'OHIO': 'INDIANA'}, inplace=True)
data
离散化和面元划分¶
为了便于分析,连续数据常常被离散化或拆分为面元(bin),即分组。
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
# 使用 cut(), 将 ages 分为 18-25,25-35.。。等几个面元(bin)
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)
cats
# 返回的是一个特殊的Categorical对象,可以看作是表示面元名称的字符串。
# 含有一个表示不同分类名称的 categories 数组以及一个 codes 属性.
cats.codes
cats.categories
pd.value_counts(cats)
# 默认为“左开右闭”,可以通过 right=False 指定“左闭右开”
pd.cut(ages, [18, 26, 36, 61, 100], right=False)
# 指定分组标签(面元名称)
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
pd.cut(ages, bins, labels=group_names)
# 按数量均分为4组,精度为2位(而不是按值划分)
data = np.random.rand(20)
pd.cut(data, 4, precision=2)
# qcut: 根据样本分位数切分。如下:根据4分位数切成4份
data = np.random.randn(1000) # Normally distributed
cats = pd.qcut(data, 4) # Cut into quartiles
cats
pd.value_counts(cats)
# 根据设定的分位数切分
pd.qcut(data, [0, 0.1, 0.5, 0.9, 1.])
检测和过滤离群点¶
离群点(outlier)的过滤或变换运算在很大程度上其实就是数组运算。
np.random.seed(12345)
data = DataFrame(np.random.randn(1000, 4))
data.describe()
# 第4列中,绝对值>3的行
col = data[3]
col[np.abs(col) > 3]
# 全部含有超过3或-3的值的行
data[(np.abs(data) > 3).any(1)]
# 修改值
data[np.abs(data) > 3] = np.sign(data) * 3
data.describe()
排列和随机采样¶
df = DataFrame(np.arange(5 * 4).reshape((5, 4)))
df
# 返回一个随机排列
sampler = np.random.permutation(5)
sampler
# 在基于ix的索引操作或者take函数中使用该数组
df.take(sampler)
# 进行截取
df.take(np.random.permutation(len(df))[:3])
bag = np.array([5, 7, -1, 6, 4])
sampler = np.random.randint(0, len(bag), size=10)
sampler
draws = bag.take(sampler)
draws
计算指标/哑变量¶
一种常用的用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量矩阵”(dummy matrix)或“指标矩阵”(indicator matrix)。
如果DataFrame的某一列有k各不同的值,可以派生出一个k列的矩阵或者DataFrame(值为1和0)。
这样的做法在下一章(第八章)的地图的例子中有体现。
df = DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'],
'data1': range(6)})
df
# 得到哑变量DataFrame
pd.get_dummies(df['key'])
# 给指标列加上一个前缀
dummies = pd.get_dummies(df['key'], prefix='key')
df_with_dummy = df[['data1']].join(dummies)
df_with_dummy
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('data/ch02/movielens/movies.dat', sep='::', header=None,
names=mnames,engine='python')
movies[:10]
# 将 `|` 分隔的 genres 拆分
genre_iter = (set(x.split('|')) for x in movies.genres)
genres = sorted(set.union(*genre_iter))
genres
# 构建一个全零的 DataFrame
dummies = DataFrame(np.zeros((len(movies), len(genres))), columns=genres)
dummies.head()
# 设置标记
for i, gen in enumerate(movies.genres):
dummies.ix[i, gen.split('|')] = 1
# 将标记合并到movies
movies_windic = movies.join(dummies.add_prefix('Genre_'))
movies_windic
movies_windic.ix[0]
# 对于很大的数据,这种方法构建指标非常慢。肯定需要编写一个能够利用DataFrame内部机制的更低级的函数才行
# 一个对统计应用的秘诀是:结合get_dummies和诸如cut之类的离散化函数
np.random.seed(12345)
values = np.random.rand(10)
values
bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
pd.get_dummies(pd.cut(values, bins))
字符串操作
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
Python有简单易用的字符串和文本处理功能。大部分文本运算直接做成了字符串对象的内置方法。当然还能用正则表达式。pandas对此进行了加强,能够对数组数据应用字符串表达式和正则表达式,而且能处理烦人的缺失数据。
字符串对象方法¶
对于大部分的字符串而言,内置的方法已经能够满足要求了。
python 的字符串方法主要有:
- count
- endswith, startswith
- join
- index
- find
- rfind
- replace
- strip, rstrip, lstrip
- split
- lower, upper
- ljust, rjust
# 返回一个列表
val = 'a,b, guido'
val.split(',')
# 去除空格
pieces = [x.strip() for x in val.split(',')]
pieces
# + 连接字符串。 注意下面的赋值方式
first, second, third = pieces
first + '::' + second + '::' + third
# 上面的不实用,下面是一种更快的风格
'::'.join(pieces)
# 字串定位,常用的有 in、index、find
'guido' in val
val.index(',')
val.find(':')
# 不包含子串会报错
# val.index(':')
# 返回个数
val.count(',')
# 替换
val.replace(',', '::')
# 剔除
val.replace(',', '')
正则表达式¶
正则表达式(regex)提供了一种灵活的在文本中搜索、匹配字符串的模式。用的是re模块。
re模块的函数分为3类:模式匹配、替换、拆分。
关于python 内置的正则表达式(re 模块),可以参考AstralWind的总结。
另外animalize 介绍了 更强大的第三方模块(regex)。
re 模块的主要方法有:
- findall, finditer
- match
- search
- split
- sub, subn
import re
text = "foo bar\t baz \tqux"
# 先编译正则表达式 \s+ (多个空白字符),然后再调用split
re.split('\s+', text)
# 等价于
# 如果想对许多字符串都应用同一条正则表达式,应该先compile节省时间
regex = re.compile('\s+')
regex.split(text)
# 找到匹配regex的所有模式 (\s+)
regex.findall(text)
text = """Dave dave@google.com
Steve steve@gmail.com
Rob rob@gmail.com
Ryan ryan@yahoo.com
"""
# r, 指定为原生字符串,使得转义字符 \ 不起作用
pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}'
# re.IGNORECASE makes the regex case-insensitive
regex = re.compile(pattern, flags=re.IGNORECASE)
#findall 返回字符串中所有匹配项
regex.findall(text)
# search只返回第一个匹配项
# 返回的是一种特殊特殊对象,这个对象只能告诉我们模式在原始字符串中的起始和结束位置
m = regex.search(text)
m
text[m.start():m.end()]
# match更加严格,它只匹配出现在字符串开头的模式
regex.match(text)
# sub方法,会将匹配到的模式替换为指定字符串,并返回新字符串
regex.sub('REDACTED', text)
# 如果想将找出的模式分段, 需要用圆括号括起来
pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})'
regex = re.compile(pattern, flags=re.IGNORECASE)
m = regex.match('wesm@bright.net')
m.groups() # 返回 tuple
regex.findall(text) # 返回列表
print(regex.sub(r'Username: \1, Domain: \2, Suffix: \3', text)) # 返回替换后的字符串
regex = re.compile(r"""
(?P<username>[A-Z0-9._%+-]+)
@
(?P<domain>[A-Z0-9.-]+)
\.
(?P<suffix>[A-Z]{2,4})""", flags=re.IGNORECASE|re.VERBOSE)
m = regex.match('wesm@bright.net')
m.groupdict() # 返回一个简单的字典
pandas中矢量化字符串函数¶
将字符串方法或正则表达式应用到一系列数据。常用的方法包括:
- cat
- contains
- count
- endswith, startswith
- findall
- get
- join
- len
- lower, upper
- match
- pad
- center
- repeat
- replace
- slice
- split
- strip, rstrip, lstrip
通过data.map()方法,所有字符串和正则都能传入各个值(通过lambda或者其他函数),但是如果存在NA就会报错。 #然而,Series有些跳过NA的方法。通过Series的str属性可以访问这些方法。
data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com',
'Rob': 'rob@gmail.com', 'Wes': np.nan}
data = Series(data)
data
data.isnull()
data.str.contains('gmail')
pattern
data.str.findall(pattern, flags=re.IGNORECASE)
matches = data.str.match(pattern, flags=re.IGNORECASE)
matches
#有两个办法可以实现矢量化的元素获取操作:要么使用str.get,要么在str属性上用索引
matches.str.get(1)
matches.str[0]
# 进行截取
data.str[:5]
示例:usda食品数据库
%pylab inline
import pandas as pd
from pandas import Series, DataFrame
import json
数据加载¶
import json
db = json.load(open('data/ch07/foods-2011-10-03.json'))
# 得到的db是个list,每个条目都是含有某种食物全部数据的字典
len(db)
db[0].keys()
# nutrients 对应的值是有关食物营养成分的一个字典列表,很长……
db[0]['nutrients'][0]
数据准备¶
# 将营养成分做成DataFrame
nutrients = DataFrame(db[0]['nutrients'])
nutrients[:7]
info_keys = ['description', 'group', 'id', 'manufacturer']
info = DataFrame(db, columns=info_keys)
info[:5]
info.head()
# 查看分类分布情况
pd.value_counts(info.group)[:10]
将所有营养成分整合到一个大表中¶
# 将列表连接起来,相当于rbind,把行对其连接在一起
nutrients = []
for rec in db:
fnuts = DataFrame(rec['nutrients'])
fnuts['id'] = rec['id']
nutrients.append(fnuts)
nutrients = pd.concat(nutrients, ignore_index=True)
nutrients.head()
去重¶
nutrients.duplicated().sum()
nutrients = nutrients.drop_duplicates()
修整¶
# 由于nutrients与info有重复的名字,所以需要重命名一下info
col_mapping = {'description' : 'food',
'group' : 'fgroup'}
info = info.rename(columns=col_mapping, copy=False)
info.head()
col_mapping = {'description' : 'nutrient',
'group' : 'nutgroup'}
nutrients = nutrients.rename(columns=col_mapping, copy=False)
nutrients.head()
数据转换¶
ndata = pd.merge(nutrients, info, on='id', how='outer')
ndata.head()
ndata.ix[30000]
建模和计算¶
# 根据营养成分,得到锌的中位数
result = ndata.groupby(['nutrient', 'fgroup'])['value'].quantile(0.5)
result['Zinc, Zn'].sort_values().plot(kind='barh')
# 发现各营养成分最为丰富的食物
by_nutrient = ndata.groupby(['nutgroup', 'nutrient'])
get_maximum = lambda x: x.xs(x.value.idxmax())
get_minimum = lambda x: x.xs(x.value.idxmin())
max_foods = by_nutrient.apply(get_maximum)[['value', 'food']]
# make the food a little smaller
max_foods.food = max_foods.food.str[:50]
max_foods.ix['Amino Acids']['food']