一分钟一个 Pandas 小技巧(三)

在逛 Kaggle 的时候发现了一篇不错的 Pandas 技巧,我将挑选一些有用的并外加一些自己的想法分享给大家。 本系列虽基础但带仍有一些奇怪操作,粗略扫一遍,您或将发现一些您需要的技巧。

纸上得来终觉浅,绝知此事要躬行,所谓的熟练使用 Pandas 是建立在您大致了解每个函数功能上,希望本系列能给您带来些许收获。

本篇所涉及知识点:

美化 DataFrame
Python 一些好玩的函数
Pandas 拼接
列中出现列表的处理方式
accessor
窗口函数
美化 DataFrame
df = pd.DataFrame([[‘上海’, ‘苹果’, 1], [‘北京’, ‘苹果’, 2], [‘天津’, ‘梨子’, 3], [
‘重庆’, ‘香蕉’, 4]], columns=[‘直辖市’, ‘水果’, ‘单价’])
df

df.style.set_caption(“美化后的 DataFrame”).format(
{“直辖市”: “{} 市”, “单价”: “¥{:.2f}”}).hide_index().background_gradient(subset=‘单价’)

[i for i in dir(pd.io.formats.style.Styler) if not i.startswith(“_”) ] 查看 style 的函数和属性。
Python 中一些好玩的函数 / 功能
zip
zip([iterable, …])

是 Python 的内置函数,用来打包多个可迭代对象的对应位置元素,返回的是元组迭代器。

str、list、tuple 都是可迭代对象
list1 = [1,2,3]
list2=[‘a’,‘b’,‘c’,‘d’]

返回的是 zip 对象

zip(list1,list2)

<zip at 0x226d8c52fc8>
在 Python3 中 zip()返回的是一个迭代器。我们需要手动的使用 list() 或者 dict() 去展示。

list(zip(list1,list2))

[(1, ‘a’), (2, ‘b’), (3, ‘c’)]
从上图我们可以发现 list1 长度是 3,list2 长度是 4,而 zip() 打包返回的列表长度是 3。所以我们可以得出结论:

zip() 打包返回的列表长度与打包对象中最短的一个对象长度相等。

zip() 可以打包,当然也可以解压。解压后的数据以同样元组方式返回。

list(zip(*zip(list1,list2)))

[(1, 2, 3), (‘a’, ‘b’, ‘c’)]
zip(*zip()) 这样的形式就可以解压了。如果看不懂,建议自己手动试一下。接下来展示几个的小例子。

list1=[1,2,3]
list2=[5,6,7]
[x*y for x,y in zip(list1,list2)]

[5, 12, 21]

list3=[‘a’,‘b’,‘c’,‘d’,‘e’]
list(zip(list3[:-1],list3[1:]))

[(‘a’, ‘b’), (‘b’, ‘c’), (‘c’, ‘d’), (‘d’, ‘e’)]
product
product(*iterables, repeat=1)

上面介绍的 zip()是对应位置打包,这个 product() 大家再来品一品。

from itertools import product
year=[2019,2020]
month=[1,2]
day=[10,15]
list(product(year,month,day))

[(2019, 1, 10),
(2019, 1, 15),
(2019, 2, 10),
(2019, 2, 15),
(2020, 1, 10),
(2020, 1, 15),
(2020, 2, 10),
(2020, 2, 15)]
product() 函数相当于是求笛卡尔积。

reduce
reduce(function, iterable[, initializer])

将可迭代对象中的前两个元素取出进行运算,将获得的值与后一位元素继续计算,以此类推。

from functools import reduce
reduce(lambda x,y:x+y,[1,3,5,7,9])

25
字典推导式
df = pd.DataFrame([‘中国’,‘加拿大’,‘墨西哥’,‘日本’,‘韩国’],columns=[‘country’])
groups = {
‘北美洲’:(‘加拿大’,‘墨西哥’),
‘亚洲’:(‘中国’,‘日本’,‘韩国’)
}
我想添加一列,返回的是国家对应的洲。顺便复习一下前一篇讲的 map。

df[‘area’] = df.country.map({x:k for k,v in groups.items() for x in v})

country area
0 中国 亚洲
1 加拿大 北美洲
2 墨西哥 北美洲
3 日本 亚洲
4 韩国 亚洲
一步步解释一下 map 里面的字典推导式。

第一步,item() 返回的是两个元组组成的列表,每个元组里面分别是一个字符串和一个元组

groups.items()

dict_items([(‘北美洲’, (‘加拿大’, ‘墨西哥’)), (‘亚洲’, (‘中国’, ‘日本’, ‘韩国’))])
第二步,通过 k(洲),v(含有国家的元组)接收 items 的 keys 和 values。

{k:v for k,v in groups.items()}

{‘北美洲’: (‘加拿大’, ‘墨西哥’), ‘亚洲’: (‘中国’, ‘日本’, ‘韩国’)}
第三步,因为要实现 Series.map 的字典替换功能,所以元组内的值(国家)应该作为最后字典的键。
对 v(含有国家的元组)进行循环获得 x(国家),此时的 x(国家)就是元组内的每个字符串,即我们需要的键。
而之前 k(洲)获取的是整个 groups 的键,这个我们需要作为最后字典的值 根据字典 "键: 值" 的格式,我们就得到了下面的字典推导式。

{x:k for k,v in groups.items() for x in v}

{‘加拿大’: ‘北美洲’, ‘墨西哥’: ‘北美洲’, ‘中国’: ‘亚洲’, ‘日本’: ‘亚洲’, ‘韩国’: ‘亚洲’}
列表生成式

生成单数列表

[i for i in range(1,11) if i%2==1 ]

[1, 3, 5, 7, 9]
Pandas 拼接
Pandas 提供了四种用于 DataFrame 拼接的方法,分别是 concat,append,merge,join。这四种的特点和区别如下表所示。

cocat
pandas.concat(objs: Union[Iterable[‘DataFrame’], Mapping[Optional[Hashable], ‘DataFrame’]], axis=‘0’, join: str = “‘outer’”, ignore_index: bool = ‘False’, keys=‘None’, levels=‘None’, names=‘None’, verify_integrity: bool = ‘False’, sort: bool = ‘False’, copy: bool = ‘True’) → ’DataFrame’

concat() 适用于 DataFrame 和 Series,是唯一一个支持行列方向拼接的函数, 只可以内外链接, 当 verify_integrity=True 时,索引如果重复将导致拼接失败。

axis=0 时,verify_integrity=True 检验行索引
axis=1 时,verify_integrity=True 检验列索引,即列名
df1 = pd.DataFrame([[‘a’, 1], [‘b’, 2]],
columns=[‘letter1’, ‘number1’])

df2 = pd.DataFrame([[‘c’, 3], [‘d’, 4]],
columns=[‘letter2’, ‘number2’])

默认 verify_integrity=False 不检验索引

pd.concat([df1,df2],axis=0,join=“outer”,verify_integrity=False)

报错,因为 df1,df2 的行索引都是 [0,1]

pd.concat([df1,df2],axis=0,join=“inner”,verify_integrity=True)

ValueError: Indexes have overlapping values: Int64Index([0, 1], dtype=‘int64’)

不报错,因为 df1,df2 的列名不一样

pd.concat([df1,df2],axis=1,join=“inner”,verify_integrity=True)

append
DataFrame.append(self, other, ignore_index=False, verify_integrity=False, sort=False) → ’DataFrame’
Series.append(self, to_append, ignore_index=False, verify_integrity=False)
append() 适用于 DataFrame 和 Series,只支持行方向拼接,案例以 Series 为例。

s1 = pd.Series([1, 2, 3])
s2 = pd.Series([4, 5, 6])

默认 verify_integrity=False 不检测 index 是否重复

s1.append(s2,verify_integrity=False)

0 1
1 2
2 3
0 4
1 5
2 6

由于 s1,s2 拥有同样的 index=[0,1,2],所以 erify_integrity=True 时会报错

s1.append(s2,verify_integrity=True)

ValueError: Indexes have overlapping values: Int64Index([0, 1, 2], dtype=‘int64’)

设置 ignore_index=True 会重置索引,否则将使用 s1,s2 自己的 index

s1.append(s2,ignore_index = True,verify_integrity=False)

0 1
1 2
2 3
3 4
4 5
5 6
如何实现交集、并集和差集
df1 = pd.DataFrame([[‘A’, ‘f’, 10], [‘B’, ‘m’, 11], [‘C’, ‘f’, 11], [
‘D’, ‘m’, 11]], columns=[‘name’, ‘sex’, ‘age’])
df2 = pd.DataFrame([[‘D’, ‘m’, 11], [‘E’, ‘f’, 9]],
columns=[‘name’, ‘sex’, ‘age’])
交集
保留两个 DataFrame 共同部分。

pd.merge(left=df1,right=df2,on=[‘name’,‘sex’,‘age’],how=‘inner’)

并集
保留两个 DataFrame 所有元素,同时去重。

pd.merge(left=df1,right=df2,on=[‘name’,‘sex’,‘age’],how=‘outer’)

差集
两个 DataFrame 不同的数据,思路是先将两个 DataFrame 拼接起来再删除重复值。

df1-df2

pd.concat([df1,df2,df2],axis=0).drop_duplicates(keep=False)

drop_duplicates
drop_duplicates(subset=None, keep=‘first’, inplace=False)
这是个比较常用的函数,用上面的案例来讲解一下这个函数,我们可以看到 df1 和 df2 只有 [‘D’, ‘m’, 11] 这行是重复的。

subset 指定判断那几列是重复的
keep 参数当发现重复行时保留哪一行, 可选 {‘first’, ‘last’, False}

判断 age 列是否重复,重复的话保留最后一行

df1.append(df2).drop_duplicates(subset=‘age’,keep=‘last’)

判断 sex 和 age 列是否同时重复,重复的话保留第一行

df1.append(df2).drop_duplicates(subset=[‘sex’,‘age’],keep=‘first’)

列中出现列表的处理方式
d = {
“Team”: [“FC Barcelona”, “FC Real Madrid”],
“Players”: [
[“Ter Stegen”, “Semedo”, “Piqué”, “Lenglet”,],
[“Courtois”, “Carvajal”, “Varane”, “Sergio Ramos”,],
],
}
df = pd.DataFrame(d)
df
根据上述字典直接生成 DataFrame 会导致第二列是列表,我们可以将其转化为一维表。

df.explode(“Players”)

accessor 访问器
Pandas 的访问器就三种,分别是.cat 用于分类数据,.str 用于字符串(对象)数据,.dt 用于类似日期时间的数据。

dt
Series.dt() 处理时间类型数据, 注意适用对象是 Series,dt 后面可以跟很多属性 / 函数。

[i for i in dir(pd.Series.dt) if not i.startswith(“_”)] 查看.dt 后面可以跟的所有函数 / 属性
df = pd.DataFrame(pd.date_range(
start=‘2020’, freq=“Q”, periods=5), columns=[‘date’])

获取年月日

df[‘year’], df[‘month’], df[‘day’] = df.date.dt.year, df.date.dt.month, df.date.dt.day

判断是否月末、年末

df[‘is_month_end’], df[‘is_year_end’] = df.date.dt.is_month_end, df.date.dt.is_year_end

判断第几周、周几

df[‘week’], df[‘weekday’], df[‘weekday_name’] = df.date.dt.week, df.date.dt.weekday, df.date.dt.weekday_name

日期格式化

df[‘strftim’] = df.date.dt.strftime(“%Y/%m/%d”)

df

当然,索引的时候你也可以用到 dt。

df[df[‘date’].dt.month==9]

快速拼接日期
有时候拿到的数据日期会分列,我们可以很轻松的将它们合并作为 index。复习一下之前提到的 product(),我们用它来创建一个 DataFrame。

from itertools import product
datecols = [‘year’,‘month’,‘day’]
df = pd.DataFrame(list(product([2019,2020],[1,2,3],[10,15,20])),columns=datecols)
df[‘value’]=np.random.randn(len(df))
df.head()

df.index = pd.to_datetime(df[datecols])
df.head()

str
Series.str() 用来处理字符串。字符串相关的功能也很多了,例如 split 切分,strip 去除空格,replace 替换以及 endswith、startswith 判断等等。一一列出太多了,所以下面用一个小案例来复习一下之前的内容,顺带大家品一下 str 的用处。

[i for i in dir(pd.Series.str) if not i.startswith(“_”)] 查看.str 后面可以跟的所有函数 / 属性
模拟饿了么的订单数据
df = pd.DataFrame([[1, ‘2020-1-1 07:00:00’, ‘煎饼1_10.00+ 鸡蛋1_2.00+ 豆浆 *1_2.50’], [2, ‘2020-1-1 08:00:20’, ‘煎饼1_10.00+ 里脊肉1_2.00+ 豆浆 *1_2.50’], [3, ‘2020-1-1 08:10:21’,
‘煎饼1_10.00+ 鸡蛋2_4.00’], [4, ‘2020-1-1 08:31:10’, ‘煎饼1_10.00+ 鸡蛋1_2.00+ 矿泉水 *1_2.50’], [5, ‘2020-1-1 08:35:12’, ‘煎饼1_10.00+ 豆浆1_2.50’], [6, ‘2020-1-1 08:37:44’, ‘煎饼1_10.00+ 里脊肉1_3.00+ 鸡蛋1_1.00+ 豆浆1_2.50’], [7, ‘2020-1-1 08:49:12’, ‘煎饼 *1_10.00’], [8, ‘2020-1-1 08:58:12’, ‘煎饼1_10.00+ 豆浆1_2.50’], [9, ‘2020-1-1 09:35:12’, ‘豆腐脑1_10.00+ 鸡蛋1_2.50’]], columns=[‘oid’, ‘datetime’, ‘order_info’])

转换日期格式

df[‘datetime’] = pd.to_datetime(df[‘datetime’])
df

复习一下前面的.dt,看看订单主要集中在那个时段?

77.8% 的订单集中在早上 8 点发生,可以看出煎饼店的话还是以早餐生意为主
df.datetime.dt.hour.value_counts(1)

8 0.777778
9 0.111111
7 0.111111
再来看一下顾客的的购物习惯。

44% 的用户单次 2 购买 3 件商品,33.3% 的顾客单次购买 3 件商品

使用 str.split 对字符串中的 "+" 进行切分, 忘记了 map 的同学可以看前一篇

df.order_info.str.split(“+”).map(len).value_counts(1)

2 0.444444
3 0.333333
4 0.111111
1 0.111111

删去时间列,仅保留订单号和订单详情

df.drop([‘datetime’],axis=1,inplace=True)

将订单详情按照 "+" 进行分割

df.order_info = df.order_info.str.split(“+”)
df.head()

我们想看一下用户在购买商品的组合上有什么倾向。复习一下如何解决列中出现列表的问题。

df = df.explode(“order_info”)
df.head(5)

接下来我们要将订单信息拆分成散列,顺便再复习一下拼接,这边展示三种方式,都可以实现这一想法。

方法一,切分 - 拼接 - 更名

使用 str.split 切分, 使用正则进行判断

expand=True 指将切分后的数据返回 DataFrame,默认 expand=False 只返回 list

pd.concat([df, df.order_info.str.split(pat=‘*|_’, expand=True)],
axis=1).rename(columns={0: ‘product’, 1: ‘quantity’, 2: ‘revenue’}).head()

方法二,查找 - 拼接

pd.concat([df, df.order_info.str.extract(
pat=‘(?P.?)*(?P\d+)_(?P.)’)], axis=1).head()

方法三,查找 - 解压 - 赋值

将订单中的购买数和销售额提取出来

df[‘product’],df[‘quantity’], df[‘revenue’] = zip(
df.order_info.apply(lambda x: re.findall(pattern="(.?)*(\d+)_(.*)", string=x)[0]))
df.head()

复习一下 groupby()。

修改数据类型

df.quantity = df.quantity.astype(int)
df.revenue = df.revenue.astype(float)

df_corr = df’oid’,’quantity’,’product’.groupby(by=[‘oid’,‘product’]).sum().unstack().fillna(value=0)

修改索引

df_corr.index= df_corr.index.rename(None)

droplevel(0) 删除 MultiIndex 的第一位

df_corr.columns=df_corr.columns.droplevel(0).rename(None)

df_corr

上面的效果呢,下面一行命令也可以实现。

squeeze 避免创建 MultiIndex

两次 rename_axis 是因为每次只能设置一个方向上的 name

df’oid’, ’quantity’, ’product’.groupby(by=[‘oid’, ‘product’]).sum().squeeze(
).unstack().rename_axis(None, axis=1).rename_axis(None, axis=0).fillna(value=0)
我们创建个热图来看一下效果。

plt.figure(figsize=(8,8))

创建热图

sns.heatmap(df_corr.corr(), cmap=“Blues”,square=False)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.title(“商品之间的组合关系”,fontdict={“size”:18},pad=20)
plt.show()

从模拟数据我们可以看出来,豆腐脑与煎饼的皮尔逊相关系数为 -1,说明没有顾客同时购买这两种产品。
皮尔逊系数较高的几对我们可以看到是煎饼 - 豆浆,豆浆 - 里脊肉,所以我们可以考虑将这三种商品结合起来作为一个套餐,减少顾客挑选时间的同时增加客单价。
cat
Categorical 是 Pandas 独有的类型,对唯一值较少的列比较友好,用来减少内存使用量。

[i for i in dir(pd.Series.cat) if not i.startswith(“_”)] 查看.cat 后面可跟的属性和函数。

比较一下 Categorical 类型的内存使用量

s_temp = pd.Series([“A”,“B”,“C”]*1000)
s_temp.nbytes # 普通的 object 类型占 24000 字节
s_temp.astype(“category”).nbytes # Category 占 3024 字节
对中文字符列进行自定义排序
先将需要排序的列转换为 category
使用 reorder_categories()或者 set_categories()
reorder_categories() 适用于自定义排序的列表与需要排序的列表长度、内容一致;
set_categories() 可以匹配不一致长度、内容的列表,但是未匹配的数据会显示 NaN
df = pd.DataFrame(
[[‘苹果’,10],[‘苹果’,50],[‘苹果’,30],[‘梨’,5],[‘梨’,20],[‘梨’,10],[‘香蕉’,12]],
columns=[“product”, “price”]
)

默认情况下是按照中文拼音的首字母进行排序的

df.sort_values(by=‘product’,ascending=True)

现在我想改成香蕉 - 梨 - 苹果这个顺序,然后再价格降序。

rule = [‘香蕉’,‘梨’,‘苹果’]
df[‘product’] = df[‘product’].astype(“category”)

reorder_categories 创建长度、内容一致的列表进行自定义排序

df[‘product’].cat.reorder_categories(rule,inplace=True)
df.sort_values(by=[‘product’,‘price’],ascending=[True,False])

创建长度、内容不一致的列表进行自定义排序, 列表中未提及的会显示 Na

rule2=[‘香蕉’,‘橘子’,‘苹果’]
df[‘product’].cat.set_categories(rule2,inplace=True)
df.sort_values(by=[‘product’,‘price’],ascending=[True,False])

窗口函数

下面通过一个 DataFrame 大家体验一下窗口函数的功能及格式。

df = pd.DataFrame([i for i in range(0, 5)], columns=[“number”])
df[“2row_mean”] = df[“number”].rolling(2).mean()
df[“3row_sum”] = df[“number”].rolling(3).sum()
df[“4row_max”] = df[“number”].rolling(4).max()
df