Pandas之二

NumPy
pandas <<=
数据可视化

Pandas 是基于 NumPy 数组构建的,特别是基于数组的函数和不适用 for 循环的数据处理,二者最大的不同是,Pandas 是专门为处理表格和混杂数据设计的,而 NumPy 更适合处理统一的数值数组的数据。
Pandas 是个比较的库了,第二版里边大约是根据 ETL 的方式在组织着,并且举了更多的例子,由于篇幅太大,很适用。这里将 Pandas 分成 2 篇,一篇基础,包括数据类型、索引、存储与加载,另外一篇是进阶,包括清理、转换、聚合

本文是 pandas 的第二篇,主要介绍数据的清理、转换、聚合

数据清洗

这里讲的数据清理主要是是对缺失数据的处理。对于缺失数据,有 2 种处理方法,drop 或者 fill

方法 说明
dropna 过滤缺失
fillna 用指定值或插值方法填充
1
2
3
4
5
6
7
8
9
10
11
from numpy import nan as NA
df = pd.DataFrame(np.random.randn(7,3))
df.iloc[:4, 1] = NA
df.dropna() # 注意:返回的也是一种复制,df本身没变
df.dropna(inplace=True) # 这时候就是原址删除
df.drop(axis=1, how='all') # 在列轴上,如果全是NA,就drop

df.fillna(0)
df.fillna(0, inplace=True) # 原址填充
df.fillna(method='ffill') # 前一数据相同填充
df.fillna(df.mean()) # 平均值填充

除此之外,还有删除重复数据的函数:drop_duplicates,默认情况是全部列都重复,也可以指定一些列

1
2
df.drop_duplicates()
df.drop_duplicates(['column1','column2'])

数据转换

转换方式太多了,这里就举几个常用的例子

肉分类

有一组关于肉类的数据,以及这些肉来自哪些动物的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data = pd.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]})

meat_to_animal = {
'bacon': 'pig',
'pulled pork': 'pig',
'pastrami': 'cow',
'corned beef': 'cow',
'honey ham': 'pig',
'nova lox': 'salmon'
}

lowercased = data['food'].str.lower()

data['animal'] = lowercased.map(meat_to_animal)
data['food'].map(lambda x: meat_to_animal[x.lower()])

利用 map,对元素级元素进行处理
这里例子是 left join 的操作,关于 join 操作,在聚合部分会细讲一下

年龄分组

分组是 sql 的说法,这里叫离散化与面元(bin)划分,在 es 中是 bucket
比如有一组人员年龄的数据,要求将它们划分到不同的年龄组

1
2
3
4
5
6
7
8
9
10
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
bins = [18, 25, 35, 60, 100]

cats = pd.cut(ages, bins) # 对每个ages给出区间,这个的cats指的是categorical
cats.categories # 区间
cats.codes # 区间所在位置
pd.value_counts(cats) # 面元计数,各个区间有多少人

group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior'] # 对各个组打标志
pd.cut(ages, bins, labels=group_names)

过滤异常

这是利用 Numpy 的元素级操作的特性,来进行数据的检索
如一个含有正态分布数据的 DataFrame,要这都出其中绝对值大于 3 的值

1
2
3
4
5
6
7
8
data = pd.DataFrame(np.random.randn(1000, 4))
data.describe()

col = data[2]
col[np.abs(col) > 3]
data[(np.abs(data) > 3).any(1)] # data中任意大于1

data[np.abs(data) > 3] = np.sign(data) * 3 # 对限制进行控制

邮箱查询

查询字符,并且可以使用正则

1
2
3
4
5
6
7
8
9
10
11
12
data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com','Rob': 'rob@gmail.com', 'Wes': np.nan}

data = pd.Series(data)

data.str.contains('gmail')
# Dave False
# Rob True
# Steve True
# Wes NaN

pattern = '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
data.str.findall(pattern, flags=re.IGNORECASE)

合并

这里的聚合数据,主要对应 SQL 的各种 join,主要有 3 个函数:merge、join、concate,还有一个特别的 combie_first 。
另外要介绍几个重塑的函数。

merge

1
2
3
4
5
6
7
df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'data1': range(6)})
df2 = pd.DataFrame({'key': ['a', 'b', 'a', 'b', 'd'],'data2': range(5)})

pd.merge(df1, df2) # merge本身是inner join,没有指定列,默认将重复的列当做键
pd.merge(df1, df2, on='key', how='left') # 指明了join的方式,以及键

pd.merge(df1, df2, left_on='key', right_on='key') # 两边可以任意指定列进行merge(join)。多列时,left_on、right_on可以是列数组

除了列与列之间 merge,列还可以与 index 进行 merge

1
2
3
4
5
6
7
8
9
10
11
lefth = pd.DataFrame({
'key1': ['Ohio', 'Ohio', 'Ohio','Nevada', 'Nevada'],
'key2': [2000, 2001, 2002, 2001,2002],
'data': np.arange(5.)
})

righth = pd.DataFrame(np.arange(12).reshape((6, 2)),
index=[['Nevada', 'Nevada', 'Ohio', 'Ohio','Ohio', 'Ohio'],[2001, 2000, 2000, 2000, 2001, 2002]],
columns=['event1', 'event2'])

pd.merge(lefth, righth, left_on=['key1', 'key2'], right_index=True)

righth 的 index 是层次索引,也就是 2 层索引,针对 lefth 中的 key1 与 key2

join

join 与 merge 太雷同了,这里只举一个简单例子

1
2
3
4
5
left = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]], index=['a', 'c', 'e'], columns=['Ohio', 'Nevada'])

right = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.],[13, 14]], index=['b', 'c', 'd', 'e'], columns=['Missouri', 'Alabama'])

left.join(right) # 与merge不同,join默认就是left join

concate

merge 是列向的增加,concate2 个轴向上都可以进行连接,也举几个简单例子

1
2
3
4
5
6
7
8
9
10
11
12
s1 = pd.Series([0, 1], index=['a', 'b'])
s2 = pd.Series([2, 3, 4], index=['c', 'd', 'e'])
s3 = pd.Series([5, 6], index=['f', 'g'])

pd.concat([s1, s2, s3]) # 默认是增加行
pd.concate([s1,s2,s3], axis=1) # 这样是在列


df1 = pd.DataFrame(np.random.randn(3, 4), columns=['a', 'b', 'c', 'd'])
df2 = pd.DataFrame(np.random.randn(2, 3), columns=['b', 'd', 'a'])

pd.concat([df1, df2], ignore_index=True)

combine_first

combine_first 是 Numpy 的 Where 的 Pandas 版,也就是 3 目运算的 Panads 版

1
2
3
4
5
6
7
8
9
10
11
12
df1 = pd.DataFrame({
'a': [1., np.nan, 5., np.nan],
'b': [np.nan, 2., np.nan, 6.],
'c': range(2, 18, 4)
})

df2 = pd.DataFrame({
'a': [5., 4., np.nan, 3., 7.],
'b': [np.nan, 3., 4., 6., 8.]
})

df1.combine_first(df2)

stack 与 unstack

介绍 stack 与 unstack 之前,先介绍一下层次化索引

层次化索引使你能在一个轴上拥有多个(两个以上)索引级别。抽象点说,它使你能以低维度形式处理高维度数据。
举一个 Series 的例子:

1
2
3
4
5
6
7
data = pd.Series(
np.random.randn(9),
index=[
['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'],
[1, 2, 3, 1, 3, 1, 2, 2, 3]
]
)

这时候 index 就有 2 层的索引
stack,将数据的列“旋转”为行,能将列索引,变成层次化行索引
unstack,将数据的行“旋转”为列,能将层次化索引,变成列索引

1
2
3
4
5
6
7
8
data = pd.DataFrame(
np.arange(6).reshape((2, 3)),
index=pd.Index(['Ohio','Colorado'], name='state'),
columns=pd.Index(['one', 'two', 'three'],name='number')
)

result = data.stack()
result.unstack()

分组聚合

pandas 提供了功能丰富的分组聚合的方法,这个 GroupBy 机制是:“split -> apply -> combine”,与 MapReduce 的思想是一致的。与 SQL 相比,它的 GroupBy 更多样,不仅可以对列分组,还可以通过函数进行分组、映射分组;它的聚合简直太丰富了,除了提供的基本聚合函数,可以自定函数。
本节,先简单介绍功能的时候,然后着重介绍一些示例

分组GroupBy

GroupBy 本身只是分组,并不会进行聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
df = pd.DataFrame({
'key1' : ['a', 'a', 'b', 'b', 'a'],
'key2' : ['one', 'two', 'one', 'two', 'one'],
'data1' : np.random.randn(5),
'data2' : np.random.randn(5)})

grouped = df['data1'].groupby(df['key1']) # 进行分组
grouped.mean() # 进行计算

means = df['data1'].groupby([df['key1'], df['key2']]).mean() # 对2列进行分组

df.groupby(['key1', 'key2'])['data1'].mean() # pandas提供了更方便的语法,先group,在选列

for name, group in df.groupby('key1'): # groupBy支持迭代,产生一个二元的元组
print(name)
print(group)

pieces = dict(list(df.groupby('key1'))) # 可以将它转换成一个字典
pieces['b']

groupby 默认是在 axis=0 上进行分组,它也可以在 axis=1 上分组.

1
grouped = df.groupby(df.dtypes, axis=1)  # 根据dtyp对列进行分组

除了通过列进行分组,还可以通过字典进行分组:

1
2
3
4
5
6
7
8
people = pd.DataFrame(
np.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])

mapping = {'a': 'red', 'b': 'red', 'c': 'blue','d': 'blue', 'e': 'red', 'f' : 'orange'}

by_column = people.groupby(mapping, axis=1).sum() # 对列进行重新映射,然后分组。字典也可以用Series

还可以通过函数进行分组

1
people.groupby(len).sum()           # 这时候是对索引执行len(),然后分组求和

聚合agg

这里对聚合给出了一个定义:聚合指的是任何能够从数组产生标量值的数据转换过程。
以这个定义,聚合函数包括:

函数名 说明
count 非NA值的数量
sum 求和
mean 平均值
median 中位数
std 标准差
var 方差
min、max 最值
prod 乘积

这些函数可以直接跟在groupby之后,进行聚合。除此之外,提供了agg函数,可以根据自定义的函数进行聚合。

1
2
3
4
def peak_to_peak(arr)
return arr.max() - arr.min()

df.groupby('key1').agg(peak_to_peak)

apply

apply应该是agg的底层(个人见解),可以将更广泛的函数应用到聚合上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tips = pd.DataFrame({
"total_bill":[16.99, 10.34, 21.01, 23.68],
"tip":[1.01, 1.66, 3.50, 3.31],
"smoker": ["No", 'No', "Yes", "Yes"],
"day": ["Sun", "Friday", "Monday", "Sun"],
"time":["Dinner", "Dinner", "Dinner", "Dinner"],
"size": [2, 3, 3, 2]
}) # 这个数据应该很大,这里只做说明用

tips['tip_pct'] = tips['tip'] / tips['total_bill']

def top(df, n=5, column='tip_pct'): # 某一列的top N
return df.sort_values(by=column)[-n:]
tips.groupby('smoker').apply(top) # 分组top N
tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill') # top 的参数可以放在后边一起传入

一些示例

  • 桶分析

    通过与cut结合,实现对数据集的桶分析

    1
    2
    3
    4
    5
    6
    7
    8
    frame = pd.DataFrame({'data1': np.random.randn(1000), 'data2': np.random.randn(1000)})
    quartiles = pd.cut(frame.data1, 4) # 切成4份,返回每个元素所在的区间
    quartiles.value_counts() # 看每份有多少个

    def get_stats(group):
    return {'min': group.min(), 'max': group.max(), 'count':group.count(), 'mean':group.mean() }

    frame.data1.groupby(quartiles).apply(get_stats) # 对每组数据,获取最值、均值、个数
  • 用特定于分组的值填充缺失

    需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数即可

    1
    2
    3
    4
    5
    6
    7
    states = ['Ohio', 'New York', 'Vermont', 'Florida','Oregon', 'Nevada', 'California', 'Idaho']
    group_key = ['East'] * 4 + ['West'] * 4
    data = pd.Series(np.random.randn(8), index=states)

    data[['Vermont', 'Nevada', 'Idaho']] = np.nan
    fill_mean = lambda g: g.fillna(g.mean())
    data.groupby(group_key).apply(fill_mean) # 根据group_key分组,并执行fill_mean
  • 随机采样

    需要从一个大数据集中随机抽取样本进行分析,使用Series的sample方法
    下面例子是模拟一副扑克牌,从每个花色随机取2张牌

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # Hearts, Spades, Clubs, Diamonds
    suits = ['H', 'S', 'C', 'D']
    card_val = (list(range(1, 11)) + [10] * 3) * 4 # card_val是21点中计分的点数
    base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q']
    cards = []
    for suit in ['H', 'S', 'C', 'D']:
    cards.extend(str(num) + suit for num in base_names) # num是牌大小,suit是花色
    deck = pd.Series(card_val, index=cards) # 每一张牌的点数

    def draw(deck, n=5):
    return deck.sample(n)

    get_suit = lambda card: card[-1] # 花色是最后字母

    deck.groupby(get_suit).apply(draw, n=2) # 以花色分组,然后各取2张牌
  • 分组加权平均

    加权平均在实际分析中用的就比较多了,这里给出分组加权平均的示例

    1
    2
    3
    4
    5
    6
    7
    df = pd.DataFrame({
    'category': ['a', 'a', 'a', 'a','b', 'b', 'b', 'b'],
    'data': np.random.randn(8),
    'weights': np.random.rand(8)})

    get_wavg = lambda group: np.average(group['data'], weights=group['weights'])
    df.groupby('category').apply(get_wavg)
  • 相关系数

    相关系数的统计实际中使用更多,刚才小费例子中,小费率与因素之间的关系都可以用相关系数来分析。
    这里举一个股票的例子 :计算由日收益率与SPX之间的年度相关系数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    close_px = pd.DataFrame({
    'AAPL': [400.29, 402.19, 408.43, 422.00],
    'MSFT': [27.00, 26.96, 27.18, 27.27],
    'XOM': [76.27, 77.16, 76.37, 78.11],
    'SPX': [1195.54, 1207.25, 1203.66, 1224.58]},
    index = ['2011-10-11', '2012-10-12', '2012-10-13', '2013-10-14']
    ) # 本数据只作为演示结构用

    spx_corr = lambda x: x.corrwith(x['SPX'])
    rets = close_px.pct_change().dropna()
    get_year = lambda x: x[0:4] # 取年

    by_year = rets.groupby(get_year)
    by_year.apply(spx_corr) # 以年做了分组

结尾

本文主要介绍了pandas的使用,从清洗、转换到聚合使用,除了上文的加载,基本符合ETL的路数。用python进行数据分析,最佳感触是函数都是元素级的,不用费力去写遍历。pandas在numpy基础上进行了封装,使其更像数据表,更容易在工程中使用。
有一点没有搞清楚,实际过程中,DataFrame可以有多大?效率怎么样?这些问题需要在实践中去考察了,pandas就到这里了。