课程资料《pandas数据处理与分析》、github地址、讲解视频、习题参考答案 、pandas官网
传送门:
- datawhale8月组队学习《pandas数据处理与分析》(上)(基础、索引、分组)
- datawhale8月组队学习《pandas数据处理与分析》(中)(变形、连接、缺失数据)
str
对象是定义在 Index
或 Series
上的属性,专门用于处理每个元素的文本内容,其内部定义了大量方法,因此对一个序列进行文本处理,首先需要获取其 str
对象。在Python标准库中也有 str
模块,为了使用上的便利,在 pandas 的50个 str
对象方法中,有31个是和标准库中的 str
模块方法同名且功能一致,例如字母转为大写的操作:
var = 'abcd'
str.upper(var) # Python内置str模块
Out[4]: 'ABCD'
s = pd.Series(['abcd', 'efg', 'hi'])
s.str
Out[6]: <pandas.core.strings.accessor.StringMethods at 0x2b796892d60>
s.str.upper() # pandas中str对象上的upper方法
Out[7]:
0 ABCD
1 EFG
2 HI
dtype: object
8.1.2 []索引器
对于 str 对象而言,可理解为其对字符串进行了序列化的操作,例如在一般的字符串中,通过 [] 可以取出某个位置的元素,同时也能通过切片得到子串。
pandas中过对 str
对象使用 [] 索引器,可以完成完全一致的功能,并且如果超出范围则返回缺失值:
s.str[0]
Out[10]:
0 a
1 e
2 h
dtype: object
s.str[-1: 0: -2]
Out[11]:
0 db
1 g
2 i
dtype: object
s.str[2]
Out[12]:
0 c
1 g
2 NaN
dtype: object
import numpy as np
import pandas as pd
s = pd.Series(['abcd', 'efg', 'hi'])
s.str[0]
0 a
1 e
2 h
dtype: object
在上一章提到,从 pandas 的 1.0.0 版本开始,引入了 string 类型,其引入的动机在于:原来所有的字符串类型都会以 object 类型的 Series 进行存储,但 object 类型只应当存储混合类型,例如同时存储浮点、字符串、字典、列表、自定义类型等,因此字符串有必要同数值型或 category 一样,具有自己的数据存储类型,从而引入了 string 类型。
总体上说,绝大多数对于 object 和 string 类型的序列使用 str 对象方法产生的结果是一致,但是在下面提到的两点上有较大差异:
string
类型和 object
类型对它们的序列化方式不同,序列化后str
对象返回结果也可能不同。例如:s = pd.Series([{1: 'temp_1', 2: 'temp_2'}, ['a', 'b'], 0.5, 'my_string'])
s
0 {1: 'temp_1', 2: 'temp_2'}
1 [a, b]
2 0.5
3 my_string
dtype: object
s.str[1] # 对每个元素取[1]的操作
0 temp_1
1 b
2 NaN
3 y
dtype: object
s.astype('string').str[1]
0 1
1 '
2 .
3 y
dtype: string
除了最后一个字符串元素,前三个元素返回的值都不同,其原因在于:
object
时,是对于每一个元素进行 [] 索引,因此对于字典而言,返回temp_1字符串,对于列表则返回第二个值,而第三个为不可迭代对象,返回缺失值,第四个是对字符串进行 [] 索引。str
对象先把整个元素转为字面意义的字符串,例如对于列表而言,第一个元素即 “{”,而对于最后一个字符串元素而言,恰好转化前后的表示方法一致,因此结果和 object
类型一致。s = pd.Series(['a'])
s.str.len()
Out[17]:
0 1
dtype: int64
s.astype('string').str.len()
Out[18]:
0 1
dtype: Int64
s == 'a'
Out[19]:
0 True
dtype: bool
s.astype('string') == 'a'
Out[20]:
0 True
dtype: boolean
s = pd.Series(['a', np.nan]) # 带有缺失值
s.str.len()
Out[22]:
0 1.0
1 NaN
dtype: float64
s.astype('string').str.len()
Out[23]:
0 1
1 <NA>
dtype: Int64
s == 'a'
Out[24]:
0 True
1 False
dtype: bool
s.astype('string') == 'a'
Out[25]:
0 True
1 <NA>
dtype: boolean
对于全体元素为数值类型的序列,即使其类型为 object 或者 category 也不允许直接使用 str 属性。如果需要把数字当成 string 类型处理,可以使用 astype 强制转换为 string 类型的 Series :
s = pd.Series([12, 345, 6789])
s.astype('string').str[1]
Out[27]:
0 2
1 4
2 7
dtype: string
这一节的两个表格来自于 learn-regex-zh 这个关于正则表达式项目,其使用 MIT 开源许可协议。这里只是介绍正则表达式的基本用法,需要系统学习的读者可参考《Python3 正则表达式》,或者《 正则表达式必知必会 》这本书
正则表达式是一种按照某种正则模式,从左到右匹配字符串中内容的一种工具。对于一般的字符而言,它可以找到其所在的位置,这里为了演示便利,使用了 python 中 re 模块的 findall
函数来匹配所有出现过但不重叠的模式,第一个参数是正则表达式,第二个参数是待匹配的字符串。例如,在下面的字符串中找出 apple :
import re
re.findall(r'Apple', 'Apple! This Is an Apple!') # 字符串从左到右依次匹配
Out[29]: ['Apple', 'Apple']
元字符 | 描述 |
---|---|
. | 匹配除换行符以外的任意字符 |
[ ] | 字符类,匹配方括号中包含的任意字符 |
[^ ] | 否定字符类,匹配方括号中不包含的任意字符 |
* | 匹配前面的子表达式零次或多次 |
+ | 匹配前面的子表达式一次或多次。比如r’d+'就是匹配数字串,r’d’就是匹配单个数字 |
? | 匹配前面的子表达式零次或一次,非贪婪方式 |
{n,m} | 花括号,匹配 n 到 m 次由前面的正则表达式定义的片段,贪婪方式 |
(xyz) | 字符组,按照确切的顺序匹配字符xyz |
| | 分支结构,匹配符号之前的字符或后面的字符 |
\ | 转义符,它可以还原元字符原来的含义 |
^ | 匹配行的开始 |
$ | 匹配行的结束 |
import re
re.findall(r'.', 'abc')
Out[30]: ['a', 'b', 'c']
re.findall(r'[ac]', 'abc') # []中有的子串都匹配
Out[31]: ['a', 'c']
re.findall(r'[^ac]', 'abc')
Out[32]: ['b']
re.findall(r'[ab]{2}', 'aaaabbbb') # {n}指匹配n次
Out[33]: ['aa', 'aa', 'bb', 'bb']
re.findall(r'aaa|bbc|ca', 'aacabbcbbc') # 匹配前面的或者后面的字符串
Out[34]: ['ca', 'bbc', 'bbc']
# 上面的元字符都有特殊含义,要匹配其本来的意思就得用\进行转义。
"""
1. ?匹配的是前一个字符,即被转义的\,所以|前面的内容就是匹配a\或者a,但是结果里面没有a\,相当于只能匹配a。
2. |右边是a\*,转义之后匹配a*,对于竖线而言左边优先级高于右边
3. 然后看目标字符串aa?a*a,第一个a匹配左边,第二个a匹配左边,第三个a虽然后面有*,
但是左边优先级高, 还是匹配左边,剩下一个a还是左边,所以结果是四个a
"""
re.findall(r'a\\?|a\*', 'aa?a*a') # 第二次先匹配到a,就不会匹配a?。a*同理。
Out[35]: ['a', 'a', 'a', 'a']
# 这里匹配不到是因为目标串'aa\a*a'中,\a是python的转义字符(\a\b\t\n等),所以匹配不到。
# 如果是'aa\s*a'之内非python的转义字符,或者'aa\\s*a',或者r'aa\\s*a'就可以匹配到\字符。
re.findall(r'\\', 'aa\a*a')
[]
re.findall(r'a?.', 'abaacadaae')
Out[36]: ['ab', 'aa', 'c', 'ad', 'aa', 'e']
re.findall(r'(\w+)=(\d+)', 'set width=20 and height=10') # 多个匹配模式,返回元组列表
[('width', '20'), ('height', '10')]
则表达式中还有一类简写字符集,其等价于一组字符的集合:
简写 | 描述 |
---|---|
\w | 匹配所有字母、数字、下划线: [a-zA-Z0-9_] |
\W | 匹配非字母和数字的字符: [^\w] |
\d | 匹配数字: [0-9] |
\D | 匹配非数字: [^\d] |
\s | 匹配空格符: [\t\n\f\r\p{Z}] |
\S | 匹配非空格符: [^\s] |
\B | 匹配非单词边界。‘er\B’ 能匹配 “verb” 中的 ‘er’,但不能匹配 “never” 中的 ‘er’。 |
re.findall(r'.s', 'Apple! This Is an Apple!')
Out[37]: ['is', 'Is']
re.findall(r'\w{2}', '09 8? 7w c_ 9q p@') # 匹配任意数字字母下划线的组合,但必须是两次
Out[38]: ['09', '7w', 'c_', '9q']
re.findall(r'\w\W\B', '09 8? 7w c_ 9q p@') # 匹配的是两个字符串,前一个是任意数字字母下划线(\W),后一个不是(\W)
Out[39]: ['8?', 'p@']
re.findall(r'.\s.', 'Constant dropping wears the stone.')
Out[40]: ['t d', 'g w', 's t', 'e s']
re.findall(r'上海市(.{2,3}区)(.{2,3}路)(\d+号)',
'上海市黄浦区方浜中路249号 上海市宝山区密山路5号')
Out[41]: [('黄浦区', '方浜中路', '249号'), ('宝山区', '密山路', '5号')]
str.split
拆分 str.split
能够把字符串的列进行拆分,其中第一个参数为正则表达式,可选参数包括从左到右的最大拆分次数 n ,是否展开为多个列 expand 。
s = pd.Series(['上海市黄浦区方浜中路249号',
'上海市宝山区密山路5号'])
s.str.split('[市区路]') # 每条结果为一行,相当于Series
Out[43]:
0 [上海, 黄浦, 方浜中, 249号]
1 [上海, 宝山, 密山, 5号]
dtype: object
s.str.split('[市区路]', n=2, expand=True) # 结果分成多个列展示,结果相当于DataFrame
Out[44]:
0 1 2
0 上海 黄浦 方浜中路249号
1 上海 宝山 密山路5号
类似的函数是 str.rsplit
,其区别在于使用 n 参数的时候是从右到左限制最大拆分次数。但是当前版本下 rsplit 因为 bug 而无法使用正则表达式进行分割:
s.str.rsplit('[市区路]', n=2, expand=True)
Out[45]:
0
0 上海市黄浦区方浜中路249号
1 上海市宝山区密山路5号
str.join
或 str.cat
合并str.join
表示用某个连接符把 Series 中的字符串列表连接起来,如果列表中出现了非字符串元素则返回缺失值。str.cat
用于合并两个序列,主要参数为:
s = pd.Series([['a','b'], [1, 'a'], [['a', 'b'], 'c']])
s.str.join('-')
Out[47]:
0 a-b
1 NaN
2 NaN
dtype: object
s1 = pd.Series(['a','b'])
s2 = pd.Series(['cat','dog'])
s1.str.cat(s2,sep='-')
Out[50]:
0 a-cat
1 b-dog
dtype: object
s2.index = [1, 2]
s1.str.cat(s2, sep='-', na_rep='?', join='outer')
Out[52]:
0 a-?
1 b-cat
2 ?-dog
dtype: object
str.contains
返回了每个字符串是否包含正则模式的布尔序列:s = pd.Series(['my cat', 'he is fat', 'railway station'])
s.str.contains('\s\wat')
0 True
1 True
2 False
dtype: bool
str.startswith
和str.endswith
返回了每个字符串以给定模式为开始和结束的布尔序列,它们都不支持正则表达式:s.str.startswith('my')
0 True
1 False
2 False
dtype: bool
s.str.endswith('t')
0 True
1 True
2 False
dtype: bool
str.match
可以用正则表达式来检测开始或结束字符串的模式,其返回了每个字符串起始处是否符合给定正则模式的布尔序列。当然,这些也能通过在str.contains
的正则中使用^
和$
来实现。(貌似没有python里的search方法)s.str.match('m|h')
s.str.contains('^[m|h]') # 二者等价
0 True
1 True
2 False
dtype: bool
s.str[::-1].str.match('ta[f|g]|n') # 反转后匹配
s.str.contains('[f|g]at|n$') # 二者等价
0 False
1 True
2 True
dtype: bool
str.find
与str.rfind
返回索引的匹配函数,其分别返回从左到右和从右到左第一次匹配的位置的索引,未找到则返回-1。需要注意的是这两个函数不支持正则匹配,只能用于字符子串的匹配:s = pd.Series(['This is an apple. That is not an apple.'])
s.str.find('apple')
Out[62]:
0 11
dtype: int64
s.str.rfind('apple')
Out[63]:
0 33
dtype: int64
str.replace
和replace
并不是一个函数,在使用字符串替换时应当使用前者。s = pd.Series(['a_1_b','c_?'])
# regex默认为True,表示是正则模式,否则第一个参数内容表示是单纯的字符串,也就是匹配字符串\d|\?
s.str.replace('\d|\?', 'new', regex=True)
0 a_new_b
1 c_new
dtype: object
当需要对不同部分进行有差别的替换时,可以利用子组
的方法,并且此时可以通过传入自定义的替换函数来分别进行处理,注意group(k)
代表匹配到的第k
个子组(圆括号之间的内容):
s = pd.Series(['上海市黄浦区方浜中路249号',
'上海市宝山区密山路5号',
'北京市昌平区北农路2号'])
pat = '(\w+市)(\w+区)(\w+路)(\d+号)'
city = {'上海市': 'Shanghai', '北京市': 'Beijing'}
district = {'昌平区': 'CP District',
'黄浦区': 'HP District',
'宝山区': 'BS District'}
road = {'方浜中路': 'Mid Fangbin Road',
'密山路': 'Mishan Road',
'北农路': 'Beinong Road'}
def my_func(m):
str_city = city[m.group(1)]
str_district = district[m.group(2)]
str_road = road[m.group(3)]
str_no = 'No. ' + m.group(4)[:-1]
return ' '.join([str_city,
str_district,
str_road,
str_no])
s.str.replace(pat, my_func, regex=True)
0 Shanghai HP District Mid Fangbin Road No. 249
1 Shanghai BS District Mishan Road No. 5
2 Beijing CP District Beinong Road No. 2
dtype: object
这里的数字标识并不直观,可以使用命名子组
更加清晰地写出子组代表的含义:
# 将各个子组进行命名
pat = '(?P<市名>\w+市)(?P<区名>\w+区)(?P<路名>\w+路)(?P<编号>\d+号)'
def my_func(m):
str_city = city[m.group('市名')]
str_district = district[m.group('区名')]
str_road = road[m.group('路名')]
str_no = 'No. ' + m.group('编号')[:-1]
return ' '.join([str_city,
str_district,
str_road,
str_no])
s.str.replace(pat, my_func, regex=True)
0 Shanghai HP District Mid Fangbin Road No. 249
1 Shanghai BS District Mishan Road No. 5
2 Beijing CP District Beinong Road No. 2
dtype: object
这里虽然看起来有些繁杂,但是实际数据处理中对应的替换,一般都会通过代码来获取数据从而构造字典映射,在具体写法上会简洁的多。
str.extract
进行提取:提取既可以认为是一种返回具体元素值(而不是布尔值或元素对应的索引位置)的匹配操作,也可以认为是一种特殊的拆分操作。前面提到的str.split
例子中会把分隔符去除,这并不是用户想要的效果,这时候就可以用str.extract
进行提取:s.str.split('[市区路]')
Out[43]:
0 [上海, 黄浦, 方浜中, 249号]
1 [上海, 宝山, 密山, 5号]
dtype: object
pat = '(\w+市)(\w+区)(\w+路)(\d+号)'
s.str.extract(pat)
Out[78]:
0 1 2 3
0 上海市 黄浦区 方浜中路 249号
1 上海市 宝山区 密山路 5号
2 北京市 昌平区 北农路 2号
通过子组的命名,可以直接对新生成DataFrame
的列命名:
pat = '(?P<市名>\w+市)(?P<区名>\w+区)(?P<路名>\w+路)(?P<编号>\d+号)'
s.str.extract(pat)
Out[79]:
市名 区名 路名 编号
0 上海市 黄浦区 方浜中路 249号
1 上海市 宝山区 密山路 5号
2 北京市 昌平区 北农路 2号
str.extractall
:不同于str.extract
只匹配一次,它会把所有符合条件的模式全部匹配出来,如果存在多个结果,则以多级索引的方式存储:s = pd.Series(['A135T15,A26S5','B674S2,B25T6'], index = ['my_A','my_B'])
pat = '[A|B](\d+)[T|S](\d+)'
s.str.extractall(pat)
Out[83]:
0 1
match
my_A 0 135 15
1 26 5
my_B 0 674 2
1 25 6
pat_with_name = '[A|B](?P\d+)[T|S](?P\d+)'
s.str.extractall(pat_with_name)
Out[84]:
name1 name2
match
my_A 0 135 15
1 26 5
my_B 0 674 2
1 25 6
str.findall
:功能类似于str.extractall
,区别在于前者把结果存入列表中,而后者处理为多级索引,每个行只对应一组匹配,而不是把所有匹配组合构成列表。s.str.findall(pat)
my_A [(135, 15), (26, 5)]
my_B [(674, 2), (25, 6)]
dtype: object
除了上述介绍的五类字符串操作有关的函数之外,str
对象上还定义了一些实用的其他方法,在此进行介绍。
upper, lower, title, capitalize, swapcase
这五个函数主要用于字母的大小写转化,从下面的例子中就容易领会其功能:
s = pd.Series(['lower', 'CAPITALS', 'this is a sentence', 'SwApCaSe'])
s.str.upper()
Out[87]:
0 LOWER
1 CAPITALS
2 THIS IS A SENTENCE
3 SWAPCASE
dtype: object
s.str.lower()
Out[88]:
0 lower
1 capitals
2 this is a sentence
3 swapcase
dtype: object
s.str.title() # 首字母大写
Out[89]:
0 Lower
1 Capitals
2 This Is A Sentence
3 Swapcase
dtype: object
s.str.capitalize() # 句首大写
Out[90]:
0 Lower
1 Capitals
2 This is a sentence
3 Swapcase
dtype: object
s.str.swapcase() # 将大写转换为小写,将小写转换为大写。
Out[91]:
0 LOWER
1 capitals
2 THIS IS A SENTENCE
3 sWaPcAsE
dtype: object
s.str.casefold() # 去除字符串中所有大小写区别
0 lower
1 capitals
2 this is a sentence
3 swapcase
这里着重需要介绍的是pd.to_numeric
方法,它虽然不是str
对象上的方法,但是能够对字符格式的数值进行快速转换和筛选。其主要参数包括:
errors
:非数值的处理模式。对于不能转换为数值的有三种errors
选项:
raise
:直接报错,默认选项coerce
:设为缺失值ignore
:保持原来的字符串。downcast
:转换类型,转成 ‘integer’, ‘signed’, ‘unsigned’, 或 ‘float’的最小dtype。比如可以转成float32就不会转成float64。s = pd.Series(['1', '2.2', '2e', '??', '-2.1', '0'])
pd.to_numeric(s, errors='ignore')
Out[93]:
0 1
1 2.2
2 2e
3 ??
4 -2.1
5 0
dtype: object
pd.to_numeric(s, errors='coerce')
Out[94]:
0 1.0
1 2.2
2 NaN
3 NaN
4 -2.1
5 0.0
dtype: float64
在数据清洗时,可以利用coerce
的设定,快速查看非数值型的行:
s[pd.to_numeric(s, errors='coerce').isna()]
Out[95]:
2 2e
3 ??
dtype: object
count
和len
的作用分别是返回出现正则模式的次数和字符串的长度:
s = pd.Series(['cat rat fat at', 'get feed sheet heat'])
s.str.count('[r|f]at|ee') # |左右两种子串都匹配了两次
Out[97]:
0 2
1 2
dtype: int64
s.str.len()
Out[98]:
0 14
1 19
dtype: int64
格式型函数主要分为两类,第一种是除空型,第二种是填充型。其中,第一类函数一共有三种,它们分别是strip, rstrip, lstrip
,分别代表去除两侧空格、右侧空格和左侧空格。这些函数在数据清洗时是有用的,特别是列名含有非法空格的时候。
my_index = pd.Index([' col1', 'col2 ', ' col3 '])
my_index.str.strip().str.len()
Out[100]: Int64Index([4, 4, 4], dtype='int64')
my_index.str.rstrip().str.len()
Out[101]: Int64Index([5, 4, 5], dtype='int64')
my_index.str.lstrip().str.len()
Out[102]: Int64Index([4, 5, 5], dtype='int64')
对于填充型函数而言,pad
是最灵活的,它可以选定字符串长度、填充的方向和填充内容:
s = pd.Series(['a','b','c'])
s.str.pad(5,'left','*')
Out[104]:
0 ****a
1 ****b
2 ****c
dtype: object
s.str.pad(5,'right','*')
Out[105]:
0 a****
1 b****
2 c****
dtype: object
s.str.pad(5,'both','*')
Out[106]:
0 **a**
1 **b**
2 **c**
dtype: object
上述的三种情况可以分别用rjust, ljust, center
来等效完成,需要注意ljust
是指右侧填充而不是左侧填充:
s.str.rjust(5, '*')
Out[107]:
0 ****a
1 ****b
2 ****c
dtype: object
s.str.ljust(5, '*')
Out[108]:
0 a****
1 b****
2 c****
dtype: object
s.str.center(5, '*')
Out[109]:
0 **a**
1 **b**
2 **c**
dtype: object
在读取excel
文件时,经常会出现数字前补0的需求,例如证券代码读入的时候会把"000007"作为数值7来处理,pandas
中除了可以使用上面的左侧填充函数进行操作之外,还可用zfill
来实现。
s = pd.Series([7, 155, 303000]).astype('string')
s.str.pad(6,'left','0')
Out[111]:
0 000007
1 000155
2 303000
dtype: string
s.str.rjust(6,'0')
Out[112]:
0 000007
1 000155
2 303000
dtype: string
s.str.zfill(6)
Out[113]:
0 000007
1 000155
2 303000
dtype: string
现有一份房屋信息数据集如下:
df = pd.read_excel('../data/house_info.xls', usecols=['floor','year','area','price'])
df.head(3)
Out[115]:
floor year area price
0 高层(共6层) 1986年建 58.23㎡ 155万
1 中层(共20层) 2020年建 88㎡ 155万
2 低层(共28层) 2010年建 89.33㎡ 365万
year
列改为整数年份存储。"""
整个序列需要先转成Nullable类型的String类型,取出年份,再将年份转为Int64类型。
注意,转换的类型是Int64不是int,否则报错。即使astype加参数errors='ignore'跳过缺失值,
转成int后,序列还有缺失值所以,还是变成了object。
而整个序列转为Int,就还是Int类型,缺失值变成了 pd.NA 。
"""
df = df.convert_dtypes()
df['year']=df['year'].str.replace('\D','',regex=True).astype('Int64')
df.loc[df.year.notna()]['year'].head()
0 1986
1 2020
2 2010
3 2014
4 2015
Name: year, Length: 12850, dtype: Int64
参考答案:
不知道为啥
pd.to_numeric(df.year.str[:-2],downcast="integer")
类型为float32,不应该是整型么
df.year = pd.to_numeric(df.year.str[:-2]).astype('Int64')
df.loc[df.year.notna()]['year']
floor
列替换为Level, Highest
两列,其中的元素分别为string
类型的层类别(高层、中层、低层)与整数类型的最高层数。pat = '(?P\w+层)(?P\(\w+层)'
df2=df['floor'].str.extract(pat) # 拆分成两列,第二列还是(共6层得形式,所以还的替换一次
df=pd.concat([df,df2],axis=1).convert_dtypes() # 新增列拼接在后面,再次转为Nullable类型
df['Highest']=df['Highest'].str.replace('\D+','',regex=True).astype('Int64')
df=df[['Level','Highest','year','area','price']]
df.head()
Level Highest year area price
0 高层 6 1986 58.23㎡ 155万
1 中层 20 2020 88㎡ 155万
2 低层 28 2010 89.33㎡ 365万
3 低层 20 2014 82㎡ 308万
4 高层 1 2015 98㎡ 117万
# 参考答案。感觉是第二个字段加了中文的()可以准备匹配出数字,但是不好直接命令子组了
pat = '(\w层)(共(\d+)层)'
new_cols = df.floor.str.extract(pat).rename(
columns={0:'Level', 1:'Highest'})
df = pd.concat([df.drop(columns=['floor']), new_cols], 1)
df.head(3)
Out[163]:
year area price Level Highest
0 1986 58.23㎡ 155万 高层 6
1 2020 88㎡ 155万 中层 20
2 2010 89.33㎡ 365万 低层 28
avg_price
,以***元/平米
的格式存储到表中,其中***
为整数。"""
str.findall返回的结果都是列表,只能用apply取值去掉列表形式
参考答案用pd.to_numeric(df.area.str[:-1])更简洁
由于area和price都没有缺失值,所以可以直接转类型
"""
df['new_area']=df['area'].str.findall(r'\d+.\d+|\d+').apply(lambda x:float(x[0]))
df['new_price']=df['price'].str.replace('\D+','',regex=True).astype('int64')
df.eval('avg_price=10000*new_price/new_area',inplace=True)
# 最后均价这一列小数转整型直接用.astype('int')就行,我还准备.apply(lambda x:int(round(x,0)))
# 最后数字+元/平米写法更简单
df['avg_price']=df['avg_price'].astype('int').astype('string')+'元/平米'
del df['new_area'],df['new_price']
df.head()
Level Highest year area price avg_price
0 高层 6 1986 58.23㎡ 155万 26618元/平米
1 中层 20 2020 88㎡ 155万 17613元/平米
2 低层 28 2010 89.33㎡ 365万 40859元/平米
3 低层 20 2014 82㎡ 308万 37560元/平米
4 高层 1 2015 98㎡ 117万 11938元/平米
# 参考答案
s_area = pd.to_numeric(df.area.str[:-1])
s_price = pd.to_numeric(df.price.str[:-1])
df['avg_price'] = ((s_price/s_area)*10000).astype(
'int').astype('string') + '元/平米'
df.head(3)
Out[167]:
year area price Level Highest avg_price
0 1986 58.23㎡ 155万 高层 6 26618元/平米
1 2020 88㎡ 155万 中层 20 17613元/平米
2 2010 89.33㎡ 365万 低层 28 40859元/平米
现有一份权力的游戏剧本数据集如下:
df = pd.read_csv('../data/script.csv')
df.head(3)
Out[115]:
Out[117]:
Release Date Season Episode Episode Title Name Sentence
0 2011-04-17 Season 1 Episode 1 Winter is Coming waymar royce What do you expect? They're savages. One lot s...
1 2011-04-17 Season 1 Episode 1 Winter is Coming will I've never seen wildlings do a thing like this...
2 2011-04-17 Season 1 Episode 1 Winter is Coming waymar royce
Episode
的台词条数。df.columns =df.columns.str.strip() # 列名中有空格
df.groupby(['Season','Episode'])['Sentence'].count().sort_values(ascending=False).head()
season Episode
Season 7 Episode 5 505
Season 3 Episode 2 480
Season 4 Episode 1 475
Season 3 Episode 5 440
Season 2 Episode 2 432
# str.count是可以计算每个字符串被正则匹配了多少次,+1就是单词数
df['len_words']=df['Sentence'].str.count(r' ')+1
df.groupby(['Name'])['len_words'].mean().sort_values(ascending=False).head()
Name
male singer 109.000000
slave owner 77.000000
manderly 62.000000
lollys stokeworth 62.000000
dothraki matron 56.666667
Name: len_words, dtype: float64
df['Sentence'].str.count(r'\?') # 计算每人提问数
ls=pd.concat([pd.Series(0),ls]).reset_index(drop=True)# 首行填0
del ls[23911] # 末行删去
df['len_questions']=ls
df.groupby(['Name'])['len_questions'].sum().sort_values(ascending=False).head()
Name
tyrion lannister 527
jon snow 374
jaime lannister 283
arya stark 265
cersei lannister 246
Name: len_questions, dtype: int64
# 参考答案
s = pd.Series(df.Sentence.values, index=df.Name.shift(-1))
s.str.count('\?').groupby('Name').sum().sort_values(ascending=False).head()
import numpy as np
import pandas as pd
在pandas
中提供了category
类型,使用户能够处理分类类型的变量,将一个普通序列转换成分类变量可以使用astype
方法。
df = pd.read_csv('data/learn_pandas.csv',
usecols = ['Grade', 'Name', 'Gender', 'Height', 'Weight'])
s = df.Grade.astype('category')
s.head()
Out[5]:
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman', 'Junior', 'Senior', 'Sophomore']
在一个分类类型的Series
中定义了cat
对象,它和上一章中介绍的str
对象类似,定义了一些属性和方法来进行分类类别的操作。
s.cat
Out[6]: <pandas.core.arrays.categorical.CategoricalAccessor object at 0x000002B7974C20A0>
cat
的属性:
cat.categories
:查看类别的本身,它以Index
类型存储cat.ordered
:类别是否有序cat.codes
:访问类别编号。每一个序列的类别会被赋予唯一的整数编号,它们的编号取决于cat.categories
中的顺序s.cat.categories
Out[7]: Index(['Freshman', 'Junior', 'Senior', 'Sophomore'], dtype='object')
s.cat.ordered
Out[8]: False
s.cat.codes.head()
Out[9]:
0 0
1 0
2 2
3 3
4 3
dtype: int8
通过cat
对象的categories
属性能够完成对类别的查询,那么应该如何进行“增改查删”的其他三个操作呢?
【NOTE】类别不得直接修改
在第三章中曾提到,索引Index
类型是无法用index_obj[0] = item
来修改的,而categories
被存储在Index
中,因此pandas
在cat
属性上定义了若干方法来达到相同的目的。
add_categories
:增加类别s = s.cat.add_categories('Graduate') # 增加一个毕业生类别
s.cat.categories
Index(['Freshman', 'Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')
remove_categories
:删除类别。同时所有原来序列中的该类会被设置为缺失。s = s.cat.remove_categories('Freshman')
s.cat.categories
Out[13]: Index(['Junior', 'Senior', 'Sophomore', 'Graduate'], dtype='object')
s.head()
Out[14]:
0 NaN
1 NaN
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Junior', 'Senior', 'Sophomore', 'Graduate']
set_categories
:直接设置序列的新类别,原来的类别中如果存在元素不属于新类别,那么会被设置为缺失。相当于索引重设。s = s.cat.set_categories(['Sophomore','PhD']) # 新类别为大二学生和博士
s.cat.categories
Out[16]: Index(['Sophomore', 'PhD'], dtype='object')
s.head()
Out[17]:
0 NaN
1 NaN
2 NaN
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (2, object): ['Sophomore', 'PhD']
remove_unused_categories
:删除未出现在序列中的类别s = s.cat.remove_unused_categories() # 移除了未出现的博士生类别
s.cat.categories
Index(['Sophomore'], dtype='object')
rename_categories
:修改序列的类别。注意,这个方法会对原序列的对应值也进行相应修改。例如,现在把Sophomore
改成中文的本科二年级学生
:s = s.cat.rename_categories({'Sophomore':'本科二年级学生'})
s.head()
0 NaN
1 NaN
2 NaN
3 本科二年级学生
4 本科二年级学生
Name: Grade, dtype: category
Categories (1, object): ['本科二年级学生']
有序类别和无序类别可以通过as_unordered
和reorder_categories
互相转化。reorder_categories
传入的参数必须是由当前序列的无序类别构成的列表,不能够新增或减少原先的类别,且必须指定参数ordered=True
,否则方法无效。例如,对年级高低进行相对大小的类别划分,然后再恢复无序状态:
s = df.Grade.astype('category')
s = s.cat.reorder_categories(['Freshman', 'Sophomore',
'Junior', 'Senior'],ordered=True)
s.head()
Out[24]:
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman' < 'Sophomore' < 'Junior' < 'Senior']
s.cat.as_unordered().head()
Out[25]:
0 Freshman
1 Freshman
2 Senior
3 Sophomore
4 Sophomore
Name: Grade, dtype: category
Categories (4, object): ['Freshman', 'Sophomore', 'Junior', 'Senior']
如果不想指定
ordered=True
参数,那么可以先用s.cat.as_ordered()
转化为有序类别,再利用reorder_categories
进行具体的相对大小调整。
在第二章中,曾提到了字符串和数值类型序列的排序。前者按照字母顺序排序,后者按照数值大小排序。
分类变量排序,只需把列的类型修改为category
后,再赋予相应的大小关系,就能正常地使用sort_index
和sort_values
。例如,对年级进行排序:
df.Grade = df.Grade.astype('category')
df.Grade = df.Grade.cat.reorder_categories(['Freshman', 'Sophomore', 'Junior', 'Senior'],ordered=True)
df.sort_values('Grade').head() # 值排序
Out[28]:
Grade Name Gender Height Weight
0 Freshman Gaopeng Yang Female 158.9 46.0
105 Freshman Qiang Shi Female 164.5 52.0
96 Freshman Changmei Feng Female 163.8 56.0
88 Freshman Xiaopeng Han Female 164.1 53.0
81 Freshman Yanli Zhang Female 165.1 52.0
df.set_index('Grade').sort_index().head() # 索引排序
Out[29]:
Name Gender Height Weight
Grade
Freshman Gaopeng Yang Female 158.9 46.0
Freshman Qiang Shi Female 164.5 52.0
Freshman Changmei Feng Female 163.8 56.0
Freshman Xiaopeng Han Female 164.1 53.0
Freshman Yanli Zhang Female 165.1 52.0
由于序的建立,因此就可以进行比较操作,方便后续索引操作。分类变量的比较操作分为两类:
==
或!=
关系的比较,比较的对象可以是标量或者同长度的Series
(或list
)。(无序时也可以比较)>,>=,<,<=
四类大小关系的比较,比较的对象和第一种类似,但是所有参与比较的元素必须属于原序列的categories
,同时要和原序列具有相同的索引。res1 = df.Grade == 'Sophomore'
res1.head()
Out[31]:
0 False
1 False
2 False
3 True
4 True
Name: Grade, dtype: bool
res2 = df.Grade == ['PhD']*df.shape[0]
res2.head()
Out[33]:
0 False
1 False
2 False
3 False
4 False
Name: Grade, dtype: bool
res3 = df.Grade <= 'Sophomore'
res3.head()
Out[35]:
0 True
1 True
2 False
3 True
4 True
Name: Grade, dtype: bool
# sample(frac=1)表示将序列随机打乱。打乱之后索引也是乱序的,直接比较会出错,必须重置索引。
res4 = df.Grade <= df.Grade.sample(frac=1).reset_index(drop=True)
res4.head()
Out[37]:
0 True
1 True
2 False
3 True
4 True
Name: Grade, dtype: bool
区间是一种特殊的类别,在实际数据分析中,区间序列往往是通过cut
和qcut
方法进行构造的,这两个函数能够把原序列的数值特征进行装箱,即用区间位置来代替原来的具体数值。
cut
函数常用参数有:bins
:最重要的参数。
n
,则表示把整个传入数组按照最大和最小值等间距地分为n
段。默认right=True
,即区间是左开右闭,需要在调整时把最小值包含进去。(在pandas
中的解决方案是在值最小的区间左端点再减去0.001*(max-min)
。)如果对序列
[1,2]
划分为2个箱子时,第一个箱子的范围(0.999,1.5]
,第二个箱子的范围是(1.5,2]
。
如果需要指定区间为左闭右开,需要把right
参数设置为False
,相应的区间调整方法是在值最大的区间右端点再加上0.001*(max-min)
。
s = pd.Series([1,2])
# bin传入整数
pd.cut(s, bins=2)
Out[39]:
0 (0.999, 1.5]
1 (1.5, 2.0]
dtype: category
Categories (2, interval[float64]): [(0.999, 1.5] < (1.5, 2.0]]
pd.cut(s, bins=2, right=False)
Out[40]:
0 [1.0, 1.5)
1 [1.5, 2.001)
dtype: category
Categories (2, interval[float64]): [[1.0, 1.5) < [1.5, 2.001)]
# bin传入分割点列表(使用`np.infty`可以表示无穷大):
pd.cut(s, bins=[-np.infty, 1.2, 1.8, 2.2, np.infty])
Out[41]:
0 (-inf, 1.2]
1 (1.8, 2.2]
dtype: category
Categories (4, interval[float64]): [(-inf, 1.2] < (1.2, 1.8] < (1.8, 2.2] < (2.2, inf]]
labels
:区间的名字retbins
:是否返回分割点(默认不返回)默认
retbins=Flase
时,返回每个元素所属区间的列表
retbins=True
时,返回的是元组,两个元素分别是元素所属区间和分割点。所属区间可再次用索引取值
s = df.Weight
res = pd.cut(s, bins=3, labels=['small', 'mid','big'],retbins=True)
res[0][:2]
Out[44]:
0 small
1 big
dtype: category
Categories (2, object): ['small' < 'big']
res[1] # 该元素为返回的分割点
Out[45]: array([0.999, 1.5 , 2. ])
qcut
函数。其用法cut
几乎没有差别,只是把bins
参数变成q
参数(quantile
)。
q
为整数n
时,指按照n
等分位数把数据分箱q
为浮点列表时,表示相应的分位数分割点。s = df.Weight
pd.qcut(s, q=3).head()
Out[47]:
0 (33.999, 48.0]
1 (55.0, 89.0]
2 (55.0, 89.0]
3 (33.999, 48.0]
4 (55.0, 89.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 48.0] < (48.0, 55.0] < (55.0, 89.0]]
pd.qcut(s, q=[0,0.2,0.8,1]).head()
Out[48]:
0 (44.0, 69.4]
1 (69.4, 89.0]
2 (69.4, 89.0]
3 (33.999, 44.0]
4 (69.4, 89.0]
Name: Weight, dtype: category
Categories (3, interval[float64]): [(33.999, 44.0] < (44.0, 69.4] < (69.4, 89.0]]
pandas的单个区间用Interval
表示,对于某一个具体的区间而言,其具备三个要素,即左端点、右端点和端点的开闭状态。
right(左开右闭), left(左闭右开), both(两边都闭), neither(两边都开)
。my_interval = pd.Interval(0, 1, 'right')
my_interval
Out[50]: Interval(0, 1, closed='right')
left,mid,right,length,closed,
,分别表示左中右端点、长度和开闭状态。
in
可以判断元素是否属于区间overlaps
可以判断两个区间是否有交集:0.5 in my_interval
True
my_interval_2 = pd.Interval(0.5, 1.5, 'left')
my_interval.overlaps(my_interval_2)
True
pd.IntervalIndex
对象有四类方法生成,分别是from_breaks, from_arrays, from_tuples, interval_range
,它们分别应用于不同的情况:
from_breaks
:类似于cut
或qcut
函数,只不过后两个是通过计算得到的分割点,而前者是直接传入自定义的分割点:pd.IntervalIndex.from_breaks([1,3,6,10], closed='both')
IntervalIndex([[1, 3], [3, 6], [6, 10]],
closed='both',
dtype='interval[int64]')
from_arrays
:分别传入左端点和右端点的列表,适用于有交集并且知道起点和终点的情况:pd.IntervalIndex.from_arrays(left = [1,3,6,10], right = [5,4,9,11], closed = 'neither')
IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
closed='neither',
dtype='interval[int64]')
from_tuples
:传入起点和终点元组构成的列表:pd.IntervalIndex.from_tuples([(1,5),(3,4),(6,9),(10,11)], closed='neither')
IntervalIndex([(1, 5), (3, 4), (6, 9), (10, 11)],
closed='neither',
dtype='interval[int64]')
interval_range
:生成等差区间。其参数有四个:start, end, periods, freq
。分别表示等差区间的起点、终点、区间个数和区间长度。其中三个量确定的情况下,剩下一个量就确定了,从而就能构造出相应的区间:pd.interval_range(start=1,end=5,periods=8) # 启起点终点和区间个数
Out[57]:
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
closed='right',
dtype='interval[float64]')
pd.interval_range(end=5,periods=8,freq=0.5) # 启起点终点和区间长度
Out[58]:
IntervalIndex([(1.0, 1.5], (1.5, 2.0], (2.0, 2.5], (2.5, 3.0], (3.0, 3.5], (3.5, 4.0], (4.0, 4.5], (4.5, 5.0]],
closed='right',
dtype='interval[float64]')
【练一练】
无论是interval_range
还是下一章时间序列中的date_range
都是给定了等差序列中四要素中的三个,从而确定整个序列。请回顾等差数列中的首项、末项、项数和公差的联系,写出interval_range
中四个参数之间的恒等关系。
除此之外,如果直接使用pd.IntervalIndex([...], closed=...)
,把Interval
类型的列表组成传入其中转为区间索引,那么所有的区间会被强制转为指定的closed
类型,因为pd.IntervalIndex
只允许存放同一种开闭区间的Interval
对象。
my_interval
Out[59]: Interval(0, 1, closed='right')
my_interval_2
Out[60]: Interval(0.5, 1.5, closed='left')
pd.IntervalIndex([my_interval, my_interval_2], closed='left')
Out[61]:
IntervalIndex([[0.0, 1.0), [0.5, 1.5)],
closed='left',
dtype='interval[float64]')
IntervalIndex
上也定义了一些有用的属性和方法。同时,如果想要具体利用cut
或者qcut
的结果进行分析,那么需要先将其转为该种索引类型:
s=df.Weight
id_interval = pd.IntervalIndex(pd.cut(s, 3)) # 返回的是每个元素所属区间,用具体数值(x,y]表示
id_interval[:3]
IntervalIndex([(33.945, 52.333], (52.333, 70.667], (70.667, 89.0]],
closed='right',
name='Weight',
dtype='interval[float64]')
Interval
类型相似,IntervalIndex
有若干常用属性:left, right, mid, length
,分别表示左右端点、两 点均值和区间长度。id_demo = id_interval[:5] # 选出前5个展示
id_demo
Out[64]:
IntervalIndex([(33.945, 52.333], (52.333, 70.667], (70.667, 89.0], (33.945, 52.333], (70.667, 89.0]],
closed='right',
name='Weight',
dtype='interval[float64]')
id_demo.left # 获取这五个区间的左端点
Out[65]: Float64Index([33.945, 52.333, 70.667, 33.945, 70.667], dtype='float64')
id_demo.right # 获取这五个区间的右端点
Out[66]: Float64Index([52.333, 70.667, 89.0, 52.333, 89.0], dtype='float64')
id_demo.mid
Out[67]: Float64Index([43.138999999999996, 61.5, 79.8335, 43.138999999999996, 79.8335], dtype='float64')
id_demo.length
Out[68]:
Float64Index([18.387999999999998, 18.334000000000003, 18.333,
18.387999999999998, 18.333],
dtype='float64')
IntervalIndex
还有两个常用方法:
contains
:逐个判断每个区间是否包含某元素overlaps
:是否和一个pd.Interval
对象有交集。id_demo.contains(50)
Out[69]: array([ True, False, False, True, False])
id_demo.overlaps(pd.Interval(40,60))
Out[70]: array([ True, True, False, True, False])
在第五章中介绍了crosstab
函数,在默认参数下它能够对两个列的组合出现的频数进行统计汇总:
df = pd.DataFrame({'A':['a','b','c','a'], 'B':['cat','cat','dog','cat']})
pd.crosstab(df.A, df.B)
Out[72]:
B cat dog
A
a 2 0
b 1 0
c 0 1
但事实上有些列存储的是分类变量,列中并不一定包含所有的类别,此时如果想要对这些未出现的类别在crosstab
结果中也进行汇总,则可以指定dropna
参数为False
:
df.B = df.B.astype('category').cat.add_categories('sheep')
pd.crosstab(df.A, df.B, dropna=False)
Out[74]:
B cat dog sheep
A
a 2 0 0
b 1 0 0
c 0 1 0
请实现一个带有dropna
参数的my_crosstab
函数来完成上面的功能。
现有一份关于钻石的数据集,其中carat, cut, clarity, price
分别表示克拉重量、切割质量、纯净度和价格,样例如下:
df = pd.read_csv('../data/diamonds.csv')
df.head(3)
Out[76]:
carat cut clarity price
0 0.23 Ideal SI2 326
1 0.21 Premium SI1 326
2 0.23 Good VS1 327
df.cut
在object
类型和category
类型下使用nunique
函数,并比较它们的性能。Fair, Good, Very Good, Premium, Ideal
,纯净度有八个等级,由次到好分别是I1, SI2, SI1, VS2, VS1, VVS2, VVS1, IF
,请对切割质量按照由好到次的顺序排序,相同切割质量的钻石,按照纯净度进行由次到好的排序。cut, clarity
这两列按照由好到次的顺序,映射到从0到n-1的整数,其中n表示类别的个数。Very Low, Low, Mid, High, Very High
,并把按这两种分箱方法得到的category
序列依次添加到原表中。先看看数据结构:
df.info()
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 carat 53940 non-null float64
1 cut 53940 non-null object
2 clarity 53940 non-null object
3 price 53940 non-null int64
dtypes: float64(1), int64(1), object(2)
%time df.cut.unique()
Wall time: 5.98 ms
array(['Ideal', 'Premium', 'Good', 'Very Good', 'Fair'], dtype=object)
%time df.cut.astype('category').unique()
Wall time: 8.01 ms # 转换类型加统计类别,一共8ms
['Ideal', 'Premium', 'Good', 'Very Good', 'Fair']
Categories (5, object): ['Ideal', 'Premium', 'Good', 'Very Good', 'Fair']
df.cut=df.cut.astype('category')
%time df.cut.unique() # 类别属性统计,2ms
Wall time: 2 ms
['Ideal', 'Premium', 'Good', 'Very Good', 'Fair']
Categories (5, object): ['Ideal', 'Premium', 'Good', 'Very Good', 'Fair']
ls_cut=['Fair', 'Good', 'Very Good', 'Premium', 'Ideal']
ls_clarity=['I1','SI2', 'SI1', 'VS2', 'VS1', 'VVS2', 'VVS1', 'IF']
df.cut=df.cut.astype('category').cat.reorder_categories(ls_cut,ordered=True) # 转换后还是得进行替换
df.clarity=df.clarity.astype('category').cat.reorder_categories(ls_clarity,ordered=True)
df.sort_values(['cut','clarity'],ascending=[False,True]).head(3)
carat cut clarity price
315 0.96 Ideal I1 2801
535 0.96 Ideal I1 2826
551 0.97 Ideal I1 2830
# 第一种是将类别重命名为整数
dict1=dict(zip(ls_cut,[x for x in range (4,-1,-1)]))
dict2=dict(zip(ls_clarity,[x for x in range (7,-1,-1)]))
df.cut=df.cut.cat.rename_categories(dict1)
df.clarity=df.clarity.cat.rename_categories(dict2)
df.head(3)
carat cut clarity price
0 0.23 0 6 326
1 0.21 1 5 326
2 0.23 3 3 327
# 第二种应该是报错object属性,然后直接进行替换
df = pd.read_csv('data/diamonds.csv')
for i,j in enumerate(ls_cut[::-1]):
df.loc[df.cut==j,'cut']=i
for k,l in enumerate(ls_clarity[::-1]):
df.loc[df.clarity==l,'clarity']=k
df.head(3)
carat cut clarity price
0 0.23 0 6 326
1 0.21 1 5 326
2 0.23 3 3 327
Very Low, Low, Mid, High, Very High
,并把按这两种分箱方法得到的category
序列依次添加到原表中。# retbins=True返回的是元组,第一个才是要的序列,第二个元素是分割点
avg=df.price/df.carat
df['price_quantile']=pd.qcut(avg, q=[0,0.2,0.4,0.6,0.8,1],
labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'],retbins=True)[0]
df['price_list']=pd.cut(avg, bins=[-np.infty,1000, 3500, 5500, 18000,np.infty],
labels=['Very Low', 'Low', 'Mid', 'High', 'Very High'],retbins=True)[0]
df.head()
carat cut clarity price price_quantile price_list
0 0.23 0 6 326 Very Low Low
1 0.21 1 5 326 Very Low Low
2 0.23 3 3 327 Very Low Low
3 0.29 1 4 334 Very Low Low
4 0.31 3 6 335 Very Low Low
分割点分别是:
array([ 1051.16 , 2295. , 3073.29, 4031.68, 5456.34, 17828.84])
array([ -inf, 1000., 3500., 5500., 18000., inf])
df['price_list'].cat.categories # 原先设定的类别数
Index(['Very Low', 'Low', 'Mid', 'High', 'Very High'], dtype='object')
df['price_list'].cat.remove_unused_categories().cat.categories # 移除未出现的类别
Index(['Low', 'Mid', 'High'], dtype='object') # 首尾两个类别未出现
avg.sort_values() # 可见首尾区间确实是没有的
31962 1051.162791
15 1078.125000
4 1080.645161
28285 1109.090909
13 1109.677419
...
26998 16764.705882
27457 16928.971963
27226 17077.669903
27530 17083.177570
27635 17828.846154
# 分割时区间不能有命名,否则字符串传入错误。
id_interval=pd.IntervalIndex(
pd.qcut(avg, q=[0,0.2,0.4,0.6,0.8,1],retbins=True)[0]
)
id_interval.left
id_interval.right
id_interval.length
import numpy as np
import pandas as pd
时间序列的概念在日常生活中十分常见,但对于一个具体的时序事件而言,可以从多个时间对象的角度来描述。例如2020年9月7日周一早上8点整需要到教室上课,这个课会在当天早上10点结束,其中包含了哪些时间概念?
会出现时间戳(Date times)的概念,即’2020-9-7 08:00:00’和’2020-9-7 10:00:00’这两个时间点分别代表了上课和下课的时刻,在pandas
中称为Timestamp
。同时,一系列的时间戳可以组成DatetimeIndex
,而将它放到Series
中后,Series
的类型就变为了datetime64[ns]
,如果有涉及时区则为datetime64[ns, tz]
,其中tz是timezone的简写。
会出现时间差(Time deltas)的概念,即上课需要的时间,两个Timestamp
做差就得到了时间差,pandas中利用Timedelta
来表示。类似的,一系列的时间差就组成了TimedeltaIndex
, 而将它放到Series
中后,Series
的类型就变为了timedelta64[ns]
。
会出现时间段(Time spans)的概念,即在8点到10点这个区间都会持续地在上课,在pandas
利用Period
来表示。类似的,一系列的时间段就组成了PeriodIndex
, 而将它放到Series
中后,Series
的类型就变为了Period
。
会出现日期偏置(Date offsets)的概念,假设你只知道9月的第一个周一早上8点要去上课,但不知道具体的日期,那么就需要一个类型来处理此类需求。再例如,想要知道2020年9月7日后的第30个工作日是哪一天,那么时间差就解决不了你的问题,从而pandas
中的DateOffset
就出现了。同时,pandas
中没有为一列时间偏置专门设计存储类型,理由也很简单,因为需求比较奇怪,一般来说我们只需要对一批时间特征做一个统一的特殊日期偏置。
通过这个简单的例子,就能够容易地总结出官方文档中的这个表格:
概念 | 单元素类型 | 数组类型 | pandas数据类型 |
---|---|---|---|
Date times | Timestamp |
DatetimeIndex |
datetime64[ns] |
Time deltas | Timedelta |
TimedeltaIndex |
timedelta64[ns] |
Time spans | Period |
PeriodIndex |
period[freq] |
Date offsets | DateOffset |
None |
None |
由于时间段对象Period/PeriodIndex
的使用频率并不高,因此将不进行讲解,而只涉及时间戳序列、时间差序列和日期偏置的相关内容。
单个时间戳的生成利用pd.Timestamp
实现,一般而言的常见日期格式都能被成功地转换:
ts = pd.Timestamp('2020/1/1')
ts
Out[4]: Timestamp('2020-01-01 00:00:00')
ts = pd.Timestamp('2020-1-1 08:10:30')
ts
Out[6]: Timestamp('2020-01-01 08:10:30')
通过year, month, day, hour, min, second
可以获取具体的数值:
ts.year
Out[7]: 2020
ts.month
Out[8]: 1
ts.day
Out[9]: 1
ts.hour
Out[10]: 8
ts.minute
Out[11]: 10
ts.second
Out[12]: 30
# 获取当前时间
now=pd.Timestamp.now()
在pandas
中,时间戳的最小精度为纳秒ns
,由于使用了64位存储,可以表示的时间范围大约可以如下计算:
T i m e R a n g e = 2 64 1 0 9 × 60 × 60 × 24 × 365 ≈ 585 ( Y e a r s ) \rm Time\,Range = \frac{2^{64}}{10^9\times 60\times 60\times 24\times 365} \approx 585 (Years) TimeRange=109×60×60×24×365264≈585(Years)
通过pd.Timestamp.max
和pd.Timestamp.min
可以获取时间戳表示的范围,可以看到确实表示的区间年数大小正如上述计算结果:
pd.Timestamp.max
Out[13]: Timestamp('2262-04-11 23:47:16.854775807')
pd.Timestamp.min
Out[14]: Timestamp('1677-09-21 00:12:43.145225')
pd.Timestamp.max.year - pd.Timestamp.min.year
Out[15]: 585
pandas.to_datetime(arg, errors='raise', dayfirst=False, yearfirst=False, utc=None, format=None,
exact=True, unit=None, infer_datetime_format=False, origin='unix', cache=True)
pandas.to_datetime将arg转换为日期时间。
arg
:可以是argint、float、str、datetime、list、tuple、一维数组、Series、DataFrame/dict-like等要转换为日期时间的对象。如果提供了 DataFrame,则该方法至少需要以下列:“年”、“月”、“日”。errors
:
- ‘raise’:默认值,无效解析将引发异常
- ‘raise’:无效解析将返回输入
- ‘coerce’:无效解析将被设置为NaTdayfirst
:如果 arg 是 str 或类似列表时使用,表示日期解析顺序
,默认为 False。如果为 True,则首先解析日期,例如“10/11/12”被解析为 2012-11-10。如果无法根据给定的 dayfirst 选项解析分隔日期字符串,会显示警告。yearfirst
:如果 arg 是 str 或类似列表时使用,表示日期解析顺序
,默认为 False。如果为 True,则首先解析年份,例如“10/11/12”被解析为2010-11-12。无法正确解析时会显示警告。(如果 dayfirst 和 yearfirst 都为 True,则 yearfirst 优先(与 dateutil 相同)。)utcbool
:默认None,控制时区相关的解析、本地化和转换。请参阅:pandas 有关时区转换和本地化的一般文档format
:str格式,默认None。时间戳的格式不满足转换时,可以强制使用format进行匹配。unitstr
:默认“ns”。它是arg (D,s,ms,us,ns) 的表示单位,可以是整数或浮点数。这将基于原点。例如,使用 unit=‘ms’ 和 origin=‘unix’ (默认值),这将计算到 unix 开始的毫秒数。
to_datetime
能够把一列时间戳格式的对象转换成为datetime64[ns]
类型的时间序列:pd.to_datetime(['2020-1-1', '2020-1-3', '2020-1-6'])
DatetimeIndex(['2020-01-01', '2020-01-03', '2020-01-06'], dtype='datetime64[ns]', freq=None)
在极少数情况,时间戳的格式不满足转换时,可以强制使用format
进行匹配:
temp = pd.to_datetime(['2020\\1\\1','2020\\1\\3'],format='%Y\\%m\\%d')
temp
DatetimeIndex(['2020-01-01', '2020-01-03'], dtype='datetime64[ns]', freq=None)
注意上面由于传入的是列表,而非pandas
内部的Series
,因此返回的是DatetimeIndex
,如果想要转为datetime64[ns]
的序列,需要显式用Series
转化:
pd.Series(temp).head()
0 2020-01-01
1 2020-01-03
dtype: datetime64[ns]
下面的序列本身就是Series
,所以不需要再转化。
df = pd.read_csv('../data/learn_pandas.csv')
s = pd.to_datetime(df.Test_Date)
s.head()
0 2019-10-05
1 2019-09-04
2 2019-09-12
3 2020-01-03
4 2019-11-06
Name: Test_Date, dtype: datetime64[ns]
to_datetime
,此时的列名必须和以下给定的时间关键词列名一致:df_date_cols = pd.DataFrame({'year': [2020, 2020],
'month': [1, 1],
'day': [1, 2],
'hour': [10, 20],
'minute': [30, 50],
'second': [20, 40]})
pd.to_datetime(df_date_cols)
0 2020-01-01 10:30:20
1 2020-01-02 20:50:40
dtype: datetime64[ns]
date_range
是一种生成连续间隔时间的一种方法,其重要的参数为start, end, freq, periods
,它们分别表示开始时间,结束时间,时间间隔,时间戳个数。其中,四个中的三个参数决定了,那么剩下的一个就随之确定了。这里要注意,开始或结束日期如果作为端点则它会被包含:pd.date_range('2020-1-1','2020-1-21', freq='10D') # 包含
Out[25]: DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21'], dtype='datetime64[ns]', freq='10D')
pd.date_range('2020-1-1','2020-2-28', freq='10D')
Out[26]:
DatetimeIndex(['2020-01-01', '2020-01-11', '2020-01-21', '2020-01-31',
'2020-02-10', '2020-02-20'],
dtype='datetime64[ns]', freq='10D')
pd.date_range('2020-1-1',
'2020-2-28', periods=6) # 由于结束日期无法取到,freq不为10天
Out[27]:
DatetimeIndex(['2020-01-01 00:00:00', '2020-01-12 14:24:00',
'2020-01-24 04:48:00', '2020-02-04 19:12:00',
'2020-02-16 09:36:00', '2020-02-28 00:00:00'],
dtype='datetime64[ns]', freq=None)
这里的freq
参数与DateOffset
对象紧密相关,将在第四节介绍其具体的用法。
【练一练】
Timestamp
上定义了一个value
属性,其返回的整数值代表了从1970年1月1日零点到给定时间戳相差的纳秒数,请利用这个属性构造一个随机生成给定日期区间内日期序列的函数。
ls=['2020-01-01','2020-02-20']
def dates(ls,n):
min=pd.Timestamp(ls[0]).value/10**9
max=pd.Timestamp(ls[1]).value/10**9
times=np.random.randint(min,max+1,n)
return pd.to_datetime(times,unit='s')
dates(ls,10)
DatetimeIndex(['2020-02-16 09:25:30', '2020-01-29 07:00:04',
'2020-01-21 12:26:02', '2020-02-08 20:34:08',
'2020-02-15 00:18:33', '2020-02-11 02:18:07',
'2020-01-12 21:48:59', '2020-01-12 00:39:24',
'2020-02-14 20:55:20', '2020-01-26 15:44:13'],
dtype='datetime64[ns]', freq=None)
asfreq
:改变序列采样频率的方法,能够根据给定的freq
对序列进行类似于reindex
的操作:s = pd.Series(np.random.rand(5),
index=pd.to_datetime([
'2020-1-%d'%i for i in range(1,10,2)]))
s.head()
Out[29]:
2020-01-01 0.836578
2020-01-03 0.678419
2020-01-05 0.711897
2020-01-07 0.487429
2020-01-09 0.604705
dtype: float64
s.asfreq('D').head()
Out[30]:
2020-01-01 0.836578
2020-01-02 NaN
2020-01-03 0.678419
2020-01-04 NaN
2020-01-05 0.711897
Freq: D, dtype: float64
s.asfreq('12H').head()
Out[31]:
2020-01-01 00:00:00 0.836578
2020-01-01 12:00:00 NaN
2020-01-02 00:00:00 NaN
2020-01-02 12:00:00 NaN
2020-01-03 00:00:00 0.678419
Freq: 12H, dtype: float64
【NOTE】datetime64[ns] 序列的极值与均值
前面提到了datetime64[ns]
本质上可以理解为一个整数,即从1970年1月1日零点到给定时间戳相差的纳秒数。所以对于一个datetime64[ns]
序列,可以使用max, min, mean
,来取得最大时间戳、最小时间戳和“平均”时间戳。
如同category, string
的序列上定义了cat, str
来完成分类数据和文本数据的操作,在时序类型的序列上定义了dt
对象来完成许多时间序列的相关操作。这里对于datetime64[ns]
类型而言,可以大致分为三类操作:取出时间相关的属性、判断时间戳是否满足条件、取整操作。
date, time, year, month, day, hour, minute, second, microsecond, nanosecond, dayofweek, dayofyear, weekofyear, daysinmonth, quarter
,其中daysinmonth, quarter
分别表示该月一共有几天和季度。s = pd.Series(pd.date_range('2020-1-1','2020-1-3', freq='D'))
s.dt.date
Out[33]:
0 2020-01-01
1 2020-01-02
2 2020-01-03
dtype: object
s.dt.time
Out[34]:
0 00:00:00
1 00:00:00
2 00:00:00
dtype: object
s.dt.day
Out[35]:
0 1
1 2
2 3
dtype: int64
s.dt.daysinmonth
Out[36]:
0 31
1 31
2 31
dtype: int64
在这些属性中,经常使用的是dayofweek
,它返回了周中的星期情况,周一为0、周二为1,以此类推。此外,还可以通过month_name, day_name
返回英文的月名和星期名,注意它们是方法而不是属性:
s.dt.dayofweek
Out[37]:
0 2
1 3
2 4
dtype: int64
s.dt.month_name()
Out[38]:
0 January
1 January
2 January
dtype: object
s.dt.day_name()
Out[39]:
0 Wednesday
1 Thursday
2 Friday
dtype: object
s.dt.is_year_start # 还可选 is_quarter/month_start
Out[40]:
0 True
1 False
2 False
dtype: bool
s.dt.is_year_end # 还可选 is_quarter/month_end
Out[41]:
0 False
1 False
2 False
dtype: bool
round, ceil, floor
,它们的公共参数为freq
,常用的包括H, min, S
(小时、分钟、秒),所有可选的freq
可参考此处。s = pd.Series(pd.date_range('2020-1-1 20:35:00',
'2020-1-1 22:35:00',
freq='45min'))
s
Out[43]:
0 2020-01-01 20:35:00
1 2020-01-01 21:20:00
2 2020-01-01 22:05:00
dtype: datetime64[ns]
s.dt.round('1H')
Out[44]:
0 2020-01-01 21:00:00
1 2020-01-01 21:00:00
2 2020-01-01 22:00:00
dtype: datetime64[ns]
s.dt.ceil('1H')
Out[45]:
0 2020-01-01 21:00:00
1 2020-01-01 22:00:00
2 2020-01-01 23:00:00
dtype: datetime64[ns]
s.dt.floor('1H')
Out[46]:
0 2020-01-01 20:00:00
1 2020-01-01 21:00:00
2 2020-01-01 22:00:00
dtype: datetime64[ns]
一般而言,时间戳序列作为索引使用。如果想要选出某个子时间戳序列,有两种方法:
dt
对象和布尔条件联合使用s = pd.Series(np.random.randint(2,size=366), index=pd.date_range('2020-01-01','2020-12-31'))
idx = pd.Series(s.index).dt
s.head()
2020-01-01 0
2020-01-02 1
2020-01-03 1
2020-01-04 0
2020-01-05 0
Freq: D, dtype: int32
Example1:每月的第一天或者最后一天
s[(idx.is_month_start|idx.is_month_end).values].head() # 必须要写.values
Out[50]:
2020-01-01 1
2020-01-31 0
2020-02-01 1
2020-02-29 1
2020-03-01 0
dtype: int32
Example2:双休日
s[idx.dayofweek.isin([5,6]).values].head()
Out[51]:
2020-01-04 1
2020-01-05 0
2020-01-11 0
2020-01-12 1
2020-01-18 1
dtype: int32
Example3:取出单日值
s['2020-01-01']
Out[52]: 1
s['20200101'] # 自动转换标准格式
Out[53]: 1
Example4:取出七月
s['2020-07'].head()
Out[54]:
2020-07-01 0
2020-07-02 1
2020-07-03 0
2020-07-04 0
2020-07-05 0
Freq: D, dtype: int32
Example5:取出5月初至7月15日
s['2020-05':'2020-7-15'].head()
Out[55]:
2020-05-01 0
2020-05-02 1
2020-05-03 0
2020-05-04 1
2020-05-05 1
Freq: D, dtype: int32
s['2020-05':'2020-7-15'].tail()
Out[56]:
2020-07-11 0
2020-07-12 0
2020-07-13 1
2020-07-14 0
2020-07-15 1
Freq: D, dtype: int32
pandas.Timedelta(value=