利用Python进行数据分析-Pandas(第四部分-数据清洗和准备)
在数据分析和建模的过程中,相当多的时间要用在数据准备上:加载、清理、转换以及重塑上。这些工作会占到分析时间的80%或更多。有时,存储在文件和数据库中的数据的格式不适合某个特定的任务。研究者都选择使用编程语言(如Python、Perl、R或Java)或UNIX文本处理工具(如sed或awk)对数据格式进行专门处理。幸运的是,pandas和内置的Python标准库提供了一组高级的、灵活的、快速的工具,可以让你轻松地将数据变为想要的格式。
在本部分,我们会讨论处理缺失数据、重复数据、字符串操作和其他分析数据转换的工具。
1、处理缺失数据
在许多数据分析工作中,缺失数据是经常发生的。pandas的目标之一就是尽量轻松地处理缺失数据。例如,pandas对象的所有描述性统计默认都不包括缺失数据。
缺失数据在pandas中呈现的方式有些不完美,但对于大多数用户可以保证功能正常。对于数值数据,pandas使用浮点值NaN(not a number)表示缺失数据。我们称其为哨兵值,可以方便的检测出来:
string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado']) print(string_data) print(string_data.isnull())
0 aardvark 1 artichoke 2 NaN 3 avocado dtype: object 0 False 1 False 2 True 3 False dtype: bool
在pandas中,我们采用了R语言中的惯用法,即将缺失值表示为NA,它表示不可用not available。在统计应用中,NA数据可能是不存在的数据或者虽然存在,但是没有观察到(例如,数据采集中发生了问题)。当进行数据清洗以进行分析时,最好直接对缺失数据进行分析,以判断数据采集的问题或缺失数据可能导致的偏差。
Python内置的None值在对象数组中也可以作为NA:
string_data[0] = None print(string_data.isnull())
0 True 1 False 2 True 3 False dtype: bool
pandas项目中还在不断优化内部细节以更好处理缺失数据,像用户API功能,例如pandas.isnull,去除了许多恼人的细节。如下表列出了一些关于缺失数据的函数:
方法 | 说明 |
dropna | 根据各标签的值中是否存在缺失数据对轴标签进行过滤,可通过阈值调节对缺失值的容忍度 |
fillna | 用指定值或插值法(如ffill或bfill)填充缺失数据 |
isnull | 返回一个含有布尔值的对象,这些布尔值表示哪些值是缺失值NA,该对象的类型与源类型一样 |
notnull | isnull的否定式 |
2、滤除缺失数据
过滤掉缺失数据的办法有很多种。可以通过pandas.isnull或布尔索引的手工方法,但dropna可能会更实用一些。对于一个Series,dropna返回一个仅含非空数据和索引值的Series:
from numpy import nan as NA data = pd.Series([1, NA, 3.5, NA, 7]) print(data.dropna())
0 1.0 2 3.5 4 7.0 dtype: float64
这等价于:
print(data[data.notnull()])
0 1.0 2 3.5 4 7.0 dtype: float64
而对于DataFrame对象,事情就有点复杂了。你可能希望丢弃全NA或含有NA的行或列。dropna默认丢弃任何含有缺失值的行:
data = pd.DataFrame([[1, 6.5, 3], [1, NA, NA], [NA, NA, NA], [NA, 6.5, 3]]) cleaned = data.dropna() print(data) print(cleaned)
0 1 2 0 1.0 6.5 3.0 1 1.0 NaN NaN 2 NaN NaN NaN 3 NaN 6.5 3.0 0 1 2 0 1.0 6.5 3.0
传入how='all'将只丢弃全为NA的那些行:
print(data.dropna(how='all'))
0 1 2 0 1.0 6.5 3.0 1 1.0 NaN NaN 3 NaN 6.5 3.0
用这种方式丢弃列,只需传入axis=1即可:
data[4] = NA print(data)
0 1 2 4 0 1.0 6.5 3.0 NaN 1 1.0 NaN NaN NaN 2 NaN NaN NaN NaN 3 NaN 6.5 3.0 NaN
print(data.dropna(axis=1, how='all'))
0 1 2 0 1.0 6.5 3.0 1 1.0 NaN NaN 2 NaN NaN NaN 3 NaN 6.5 3.0
另一个滤除DataFrame行的问题涉及时间序列数据。假设你只想留下一部分观测数据,可以用thresh参数实现此目的:
df = pd.DataFrame(np.random.randn(7, 3)) df.iloc[:4, 1] = NA df.iloc[:2, 2] = NA print(df)
0 1 2 0 -1.447767 NaN NaN 1 0.401949 NaN NaN 2 -0.387527 NaN -0.704830 3 -0.204761 NaN 0.759555 4 1.068036 0.172537 0.035865 5 -0.743482 -0.548605 1.671702 6 -1.341783 -2.349730 -0.084773
删除有空列的行:
print(df.dropna())
0 1 2 4 1.068036 0.172537 0.035865 5 -0.743482 -0.548605 1.671702 6 -1.341783 -2.349730 -0.084773
删除前面两行:
print(df.dropna(thresh=2))
0 1 2 2 -0.387527 NaN -0.704830 3 -0.204761 NaN 0.759555 4 1.068036 0.172537 0.035865 5 -0.743482 -0.548605 1.671702 6 -1.341783 -2.349730 -0.084773
3、填充缺失数据
你可能不想滤除缺失数据(有可能会丢弃跟它有关的其他数据),而是希望通过其他方式填补那些“空洞”。对于大多数情况而言,fillna方法是最主要的函数。通过一个函数调用fillna就会将缺失值替换为那个常数值:
print(df.fillna(0))
0 1 2 0 -0.121226 0.000000 0.000000 1 1.011158 0.000000 0.000000 2 -1.573227 0.000000 -0.405378 3 -0.060209 0.000000 0.158404 4 0.259625 0.336858 1.108103 5 -2.245870 -0.698832 0.671143 6 1.317690 0.096099 -0.864773
若是通过一个字典调用fillna,就可以实现对不同的列填充不同的值:
print(df.fillna({1: 0.5, 2: 0}))
0 1 2 0 1.521963 0.500000 0.000000 1 0.427367 0.500000 0.000000 2 1.104610 0.500000 0.196240 3 -0.166765 0.500000 1.241586 4 -0.822953 -0.555102 0.012395 5 -0.726751 -1.509729 0.418866 6 0.103022 -0.857646 -0.546169
fillna默认会返回新对象,但也可以对现有对象进行就地修改:
_ = df.fillna(0, inplace=True) print(df)
0 1 2 0 -0.356009 0.000000 0.000000 1 -0.745492 0.000000 0.000000 2 -0.672743 0.000000 -0.505817 3 0.769577 0.000000 1.106648 4 -0.631494 -0.033749 0.135418 5 -1.943620 0.084401 0.324903 6 0.755456 -0.269200 -1.278897
对reindexing有效的那些插值方法也可以用于fillna:
df = pd.DataFrame(np.random.randn(6, 3)) df.iloc[2:, 1] = NA df.iloc[4:, 2] = NA print(df)
0 1 2 0 -0.225238 0.440305 -1.595615 1 -1.224405 0.114891 0.524828 2 0.325263 NaN 1.071736 3 0.474343 NaN 0.099062 4 1.710168 NaN NaN 5 -0.196244 NaN NaN
向前填充:
print(df.fillna(method='ffill'))
0 -0.225238 0.440305 -1.595615 1 -1.224405 0.114891 0.524828 2 0.325263 0.114891 1.071736 3 0.474343 0.114891 0.099062 4 1.710168 0.114891 0.099062 5 -0.196244 0.114891 0.099062
填充时限定行数:
print(df.fillna(method='ffill', limit=2))
0 1 2 0 -0.225238 0.440305 -1.595615 1 -1.224405 0.114891 0.524828 2 0.325263 0.114891 1.071736 3 0.474343 0.114891 0.099062 4 1.710168 NaN 0.099062 5 -0.196244 NaN 0.099062
只要有创新,你就可以利用fillna实现许多别的功能。比如说,你可以传入Series的平均值或中位数:
data = pd.Series([1, NA, 3.5, NA, 7]) print(data.fillna(data.mean()))
0 1.000000 1 3.833333 2 3.500000 3 3.833333 4 7.000000 dtype: float64
如下表列出了fillna的参考:
参数 | 说明 |
axis | 待填充的轴,默认为axis=0 |
inplace | 修改调用者对象而不产生副本 |
limit | (对于前向和后项填充)可以连续填充的最大数量 |
4、数据转换-移除重复数据
DataFrame中出现重复行有多种原因。如下就是一个栗子:
data = pd.DataFrame({'k1': ['one', 'two'] *3 + ['two'], 'k2': [1, 1, 2, 3, 3, 4, 4]}) print(data)
k1 k2 0 one 1 1 two 1 2 one 2 3 two 3 4 one 3 5 two 4 6 two 4
DataFrame的duplicated方法返回一个布尔型Series,表示各行是否是重复行(前面出现过的行):
print(data.duplicated())
0 False 1 False 2 False 3 False 4 False 5 False 6 True dtype: bool
还有一个与此相关的drop_duplicates方法,是删除重复的,并且声称一个新的对象:
print(data.drop_duplicates())
k1 k2 0 one 1 1 two 1 2 one 2 3 two 3 4 one 3 5 two 4
这两个方法默认会判断全列,你也可以指定部分列进行重复项判断。假设我们还有一列值,且只希望根据k1列过滤重复项:
data['v1'] = range(7) print(data.drop_duplicates(['k1']))
k1 k2 v1 0 one 1 0 1 two 1 1
duplicated和drop_duplicates默认保留的是第一个出现的值的组合。传入keep=‘last’则保留最后一个:
print(data.drop_duplicates(['k1', 'k2'], keep='last'))
k1 k2 v1 0 one 1 0 1 two 1 1 2 one 2 2 3 two 3 3 4 one 3 4 6 two 4 6
5、数据转移-利用函数或映射进行数据转换
对于许多数据集,你可能希望根据数组、Series或DataFrame列中的值来实现转换工作。我们来看看下面这组有关肉类的数据:
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]}) print(data)
food ounces 0 bacon 4.0 1 pulled pork 3.0 2 bacon 12.0 3 Pastrami 6.0 4 corned beef 7.5 5 Bacon 8.0 6 pastrami 3.0 7 honey ham 5.0 8 nova lox 6.0
假设你想要添加一列表示该肉类食物来源的动物类型。我们先编写一个不同肉类到动物的映射。
meat_to_animal = {'bacon': 'pig', 'pulled pork': 'pig', 'pastrami': 'cow', 'corned beef': 'cow', 'honey ham': 'pip', 'nova lox': 'salmon'}
Series的map方法可以接受一个函数或含有映射关系的字典型对象,但是这里有一个小问题,即有些肉类的首字母大写了,而另一些则没有。因此,我们还需要使用Series的str.lower方法,将各个值转换为小写:
lowercased = data['food'].str.lower() print(lowercased)
0 bacon 1 pulled pork 2 bacon 3 pastrami 4 corned beef 5 bacon 6 pastrami 7 honey ham 8 nova lox Name: food, dtype: object
接着使用map函数用来映射:
data['animal'] = lowercased.map(meat_to_animal) print(data)
food ounces animal 0 bacon 4.0 pig 1 pulled pork 3.0 pig 2 bacon 12.0 pig 3 Pastrami 6.0 cow 4 corned beef 7.5 cow 5 Bacon 8.0 pig 6 pastrami 3.0 cow 7 honey ham 5.0 pip 8 nova lox 6.0 salmon
当然我们也可以传入一个能够完成全部这些工作的函数:
print(data['food'].map(lambda x:meat_to_animal[x.lower()]))
0 pig 1 pig 2 pig 3 cow 4 cow 5 pig 6 cow 7 pip 8 salmon Name: food, dtype: object
6、数据转换-替换值
利用fillna方法填充缺失数据可以看做值替换的一种特殊情况。前面我们已经看到,map可用于修改对象的数据子集,而replace则提供了一种实现该功能的更简单、更灵活的方式。我们来看看下面这个Series:
data = pd.Series([1, -999, 2, -999, -1000, 3]) print(data)
0 1 1 -999 2 2 3 -999 4 -1000 5 3 dtype: int64
-999这个值可能是一个表示缺失数据的标记值。要将其替换为pandas能够理解的NA值,我们可以利用replace来产生一个新的Series(除非传入inplace=True):
print(data.replace(-999, np.nan))
0 1.0 1 NaN 2 2.0 3 NaN 4 -1000.0 5 3.0 dtype: float64
如果你希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值:
print(data.replace([-999, -1000], np.nan))
0 1.0 1 NaN 2 2.0 3 NaN 4 NaN 5 3.0 dtype: float64
要让每个值有不同的替换值,可以传递一个替换列表:
print(data.replace([-999, -1000], [np.nan, 0]))
0 1.0 1 NaN 2 2.0 3 NaN 4 0.0 5 3.0 dtype: float64
传入的参数也可以是字典:
print(data.replace({-999: np.nan, -1000: 0}))
0 1.0 1 NaN 2 2.0 3 NaN 4 0.0 5 3.0 dtype: float64
7、数据转换-重命名轴索引
跟Series中的值一样,轴标签也可以通过函数或映射进行转换,从而得到一个新的不同标签的对象。轴还可以被就地修改,而无需新建一个数据结构。接下来我们看看下面这个简单的栗子:
data = pd.DataFrame(np.arange(12).reshape((3, 4)), index=['Ohio', 'Colorado', 'New York'], columns=['one', 'two', 'three', 'four']) print(data)
one two three four Ohio 0 1 2 3 Colorado 4 5 6 7 New York 8 9 10 11
跟Series一样,轴索引也有一个map方法:
transform = lambda x: x[: 4].upper() print(data.index.map(transform))
Index(['OHIO', 'COLO', 'NEW '], dtype='object')
你可以将其赋值给index,这样就可以对DataFrame进行就地修改:
data.index = data.index.map(transform) print(data)
one two three four OHIO 0 1 2 3 COLO 4 5 6 7 NEW 8 9 10 11
如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename:
print(data.rename(index=str.title, columns=str.upper))
ONE TWO THREE FOUR Ohio 0 1 2 3 Colo 4 5 6 7 New 8 9 10 11
特别说明一下,rename可以结合字典对象实现对部分轴标签的更新:
print(data.rename(index={'OHIO': 'INDIANA'}, columns={'three': 'peekaboo'}))
one two peekaboo four INDIANA 0 1 2 3 COLO 4 5 6 7 NEW 8 9 10 11
rename可以实现复制DataFrame并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入inplace=True即可:
data.rename(index={'OHIO': 'INDIANA'}, inplace=True) print(data)
one two three four INDIANA 0 1 2 3 COLO 4 5 6 7 NEW 8 9 10 11
8、数据转换-离散化和面元划分
为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
接下来将这些数据划分为“18到25”、“26到35”、“35到60”、以及“60以上”几个面元。要实现该功能,需要使用pandas的cut函数:
bins = [18, 25, 35, 60, 100] cats = pd.cut(ages, bins) print(cats)
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]] Length: 12 Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]
pandas返回的是一个特殊的Categorical对象。结果展示了pandas.cut划分的面元。你可以将其看做一组表示面元名称的字符串。它的底层含有一个表示不同分类名称的类型数组,以及一个codes属性中的年龄数据的标签:
print(cats.codes)
[0 0 0 1 0 0 2 1 3 2 2 1]
print(pd.value_counts(cats))
(18, 25] 5 (35, 60] 3 (25, 35] 3 (60, 100] 1 dtype: int64
pd.value_counts(cats)是pandas.cut结果的面元计数。
跟“区间”的数学符号一样,圆括号表示开端,方括号表示闭端(包括)。哪边是闭端可以通过right=False进行修改:
print(pd.cut(ages, [18, 26, 36, 61, 100], right=False))
[[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36, 61), [36, 61), [26, 36)] Length: 12 Categories (4, interval[int64]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)]
你可以通过传递一个列表或数组到labels,设置自己的面元名称:
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior'] print(pd.cut(ages, bins, labels=group_names))
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult] Length: 12 Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]
如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。下面这个例子中,我们将一些均匀分布的数据分成四组:
data = np.random.rand(20) print(pd.cut(data, 4, precision=2))
[(0.27, 0.48], (0.066, 0.27], (0.48, 0.69], (0.27, 0.48], (0.69, 0.9], ..., (0.066, 0.27], (0.27, 0.48], (0.27, 0.48], (0.27, 0.48], (0.066, 0.27]] Length: 20 Categories (4, interval[float64]): [(0.066, 0.27] < (0.27, 0.48] < (0.48, 0.69] < (0.69, 0.9]]
选项precision=2,限定小数只有两位。
qcut是一个非常类似与cut的函数,它可以根据样本分位数对数据进行面元划分。根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点。而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元。
data = np.random.randn(1000) cats = pd.qcut(data, 4) print(cats)
[(0.0634, 0.736], (0.736, 3.737], (-3.0029999999999997, -0.629], (-3.0029999999999997, -0.629], (-0.629, 0.0634], ..., (0.0634, 0.736], (-3.0029999999999997, -0.629],
(-3.0029999999999997, -0.629], (0.736, 3.737], (-3.0029999999999997, -0.629]] Length: 1000 Categories (4, interval[float64]): [(-3.0029999999999997, -0.629] < (-0.629, 0.0634] < (0.0634, 0.736] < (0.736, 3.737]]
print(pd.value_counts(cats))
(0.736, 3.737] 250 (0.0634, 0.736] 250 (-0.629, 0.0634] 250 (-3.0029999999999997, -0.629] 250 dtype: int64
与cut类似,你也可以传递自定义的分位数(0到1之间的数值,包含端点):
print(pd.qcut(data, [0,0.1,0.5,0.9,1. ]))
[(-1.388, -0.0335], (-0.0335, 1.339], (-0.0335, 1.339], (-1.388, -0.0335], (-2.851, -1.388], ..., (-2.851, -1.388], (-0.0335, 1.339], (-1.388, -0.0335], (-0.0335, 1.339],
(-0.0335, 1.339]] Length: 1000 Categories (4, interval[float64]): [(-2.851, -1.388] < (-1.388, -0.0335] < (-0.0335, 1.339] < (1.339, 3.383]]
9、数据转换-检测和过滤异常值
过滤或变换异常值(outliter)在很大程度上就是运用数组运算。来看一个含有正态分布数据的DataFrame:
data = pd.DataFrame(np.random.randn(1000, 4)) print(data.describe())
0 1 2 3 count 1000.000000 1000.000000 1000.000000 1000.000000 mean 0.026021 0.008996 0.010970 0.057731 std 1.002353 0.959835 1.009001 1.017688 min -3.690002 -2.713724 -3.484434 -3.010707 25% -0.648175 -0.639120 -0.649641 -0.659930 50% 0.011831 -0.004967 0.004085 0.058320 75% 0.676881 0.655670 0.674006 0.755476 max 3.151103 2.988864 3.152439 2.727467
假设你想要找出某列中绝对值大小超过3的值:
col = data[2] print(col[np.abs(col) > 3])
442 -3.484434 906 3.018734 954 3.152439 Name: 2, dtype: float64
要选出全部含有“超过3或-3的值”的行,你可以在布尔型DataFrame中使用any方法:
print(data[(np.abs(data) > 3).any(1)])
0 1 2 3 166 3.093191 -0.035867 1.269070 1.017358 442 0.549934 -1.402305 -3.484434 0.307285 594 -3.690002 0.206858 0.168607 -0.877162 630 -0.529124 0.012966 0.472111 -3.010707 730 3.016566 0.704795 0.819027 -0.316861 906 1.472665 -1.010752 3.018734 1.321745 910 3.151103 -0.329429 -0.087949 -0.119672 954 -2.291275 1.362840 3.152439 -0.182672
根据这些条件,就可以对值进行设置。下面的代码可以将值限制在区间-3到3以内:
data[np.abs(data) > 3] = np.sign(data) * 3
print(data.describe())
0 1 2 3 count 1000.000000 1000.000000 1000.000000 1000.000000 mean 0.026450 0.008996 0.011283 0.057742 std 0.999234 0.959835 1.006916 1.017655 min -3.000000 -2.713724 -3.000000 -3.000000 25% -0.648175 -0.639120 -0.649641 -0.659930 50% 0.011831 -0.004967 0.004085 0.058320 75% 0.676881 0.655670 0.674006 0.755476 max 3.000000 2.988864 3.000000 2.727467
根据数据的值是正还是负,np.sign(data)可以生成1和-1:
print(np.sign(data).head())
0 1 2 3 0 -1.0 1.0 1.0 -1.0 1 1.0 1.0 1.0 -1.0 2 1.0 -1.0 1.0 1.0 3 1.0 1.0 1.0 1.0 4 -1.0 1.0 -1.0 -1.0
10、数据转换-排列和随机采样
利用numpy.random.permutation函数可以轻松实现对Series或DataFrame的列的排列工作(permuting,随机重排序)。通过需要列排列的轴的长度调用permutation,可以产生一个表示新顺序的整数数组:
df = pd.DataFrame(np.arange(20).reshape((5, 4))) sampler = np.random.permutation(5) print(sampler)
[1 2 0 4 3]
然后就可以在基于iloc的索引操作或take函数中使用该数组了:
print(df) print(df.take(sampler))
0 1 2 3 0 0 1 2 3 1 4 5 6 7 2 8 9 10 11 3 12 13 14 15 4 16 17 18 19 0 1 2 3 1 4 5 6 7 2 8 9 10 11 0 0 1 2 3 4 16 17 18 19 3 12 13 14 15
如果不想用替换的方式选取随机子集,可以在Series和DataFrame上使用sample方法:
print(df.sample(n=3))
0 1 2 3 2 8 9 10 11 0 0 1 2 3 4 16 17 18 19
要通过替换的方式产生样本(允许重复选择),可以传递replace=True到sample:
choices = pd.Series([5, 7, -1, 6, 4]) draws = choices.sample(n=10, replace=True) print(draws)
3 6 0 5 3 6 2 -1 1 7 4 4 4 4 4 4 2 -1 4 4 dtype: int64
11、数据转换-计算指标/哑变量
另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。
如果DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)。pandas有一个get_dummies函数可以实现该功能 (其实自己动手做一个也不难)。使用之前的一个DataFrame栗子:
df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'datal': range(6)}) print(pd.get_dummies(df['key']))
a b c 0 0 1 0 1 0 1 0 2 1 0 0 3 0 0 1 4 1 0 0 5 0 1 0
有时候,你可能想给指标DataFrame的列加上一个前缀,以便能够跟其他数据进行合并。get_dummies的prefix参数可以实现该功能:
dummies = pd.get_dummies(df['key'], prefix='key') df_with_dummy = df[['datal']].join(dummies) print(df_with_dummy)
datal key_a key_b key_c 0 0 0 1 0 1 1 0 1 0 2 2 1 0 0 3 3 0 0 1 4 4 1 0 0 5 5 0 1 0
一个对统计应用有用的秘诀是:结合get_dummies和诸如cut之类的离散化函数:
np.random.seed(12345) values = np.random.rand(10) print(values) bins = [0, 0.2, 0.4, 0.6, 0.8, 1] print(pd.get_dummies(pd.cut(values, bins)))
[0.92961609 0.31637555 0.18391881 0.20456028 0.56772503 0.5955447 0.96451452 0.6531771 0.74890664 0.65356987] (0.0, 0.2] (0.2, 0.4] (0.4, 0.6] (0.6, 0.8] (0.8, 1.0] 0 0 0 0 0 1 1 0 1 0 0 0 2 1 0 0 0 0 3 0 1 0 0 0 4 0 0 1 0 0 5 0 0 1 0 0 6 0 0 0 0 1 7 0 0 0 1 0 8 0 0 0 1 0 9 0 0 0 1 0
我们使用numpy.random.seed,使这个栗子具有确定性。
12、字符串操作-字符串对象方法
python能够成为流行的数据处理语言,部分原因是其简单易用的字符串和文本处理功能。大部分文本运算都直接做成了字符串对象的内置方法。对于更复杂的模式匹配和文本操作,则可能需要用到正则表达式。pandas对此进行了加强,它是你能够对整组数据应用字符串表达式和正则表达式,而且能处理烦人的缺失数据。
对于许多字符串处理和脚本应用,内置的字符串方法已经能够满足要求了。例如,以逗号分割的字符串可以用split拆分成数段:
val = 'a,b, guido' print(val.split(','))
['a', 'b', ' guido']
split常常与strip一起使用,以去除空白符(包括换行符):
pieces = [x.strip() for x in val.split(',')] print(pieces)
['a', 'b', 'guido']
利用加法,可以将这些字符串以双冒号分隔符形式连接起来:
first, second, third = pieces print(first + '::' + second + '::' + third)
a::b::guido
但这种方式并不是很实用。一种更快更符合Python风格的方式是,向字符串“::”的join方法传入一个列表或元组:
print('::'.join(pieces))
a::b::guido
其他方法关注的是子串定位。检测子串的最佳方式是利用Python的in关键字,还可以使用index和find:
print('guido' in val)
True
print(val.index(','))
1
print(val.find(':'))
-1
需要注意的是find和index的区别:如果找不到字符串,index将会引发一个异常(而不是返回-1)
与此相关,count可以返回指定子串出现的次数:
print(val.count(','))
2
replace用于指定模式替换为另一个模式。通过传入空字符串,它也常常用于删除模式:
print(val.replace(',', '::')) print(val.replace(',', ''))
a::b:: guido ab guido
如下表列出了Python内置的字符串方法:
方法 | 说明 |
count | 返回子串在字符串中的出现次数(非重叠) |
endswith、startswith | 如果字符串以某个后缀结尾(以某个前缀开头),则返回True |
join | 将字符串用作连接其他字符串序列的分隔符 |
index | 如果在字符串中找到子串,则返回子串第一个字符所在的位置。如果没有找到,则引发valueErro |
find | 如果在字符串中找到子串,则返回第一个发现发现的子串的第一个字符所在的位置。如果没有找到,则返回-1. |
rfind | 如果在字符串中找到子串,则返回最后一个发现的子串的第一个字符所在的位置。如果没有找到,则返回-1 |
replace | 用另一个字符串替换指定子串 |
strip、rstrip、lstrip | 去除空白符(包括换行符)。相当于对各个元素执行x.strip()(以及rstrip、lstrip) |
split | 通过指定的分隔符将字符串拆分为一组子串 |
lower、upper | 分别将字母字符转换为小写或大写 |
ljust、rjust | 用空格(或其他字符)填充字符串的空白侧以返回符合最低宽度的字符串 |
13、字符串操作-正则表达式
正则表达式提供了一种灵活的在文本中搜索或匹配(通常比前者复杂)字符串模式的方式。正则表达式,常称作regex,是根据正则表达式语言编写的字符串。Python内置的re模块负责对字符串应用正则表达式。
re模块的函数可以分为三大类:模式匹配、替换以及拆分。当然,它们之间是相辅相成的。一个regex描述了需要在文本中定位的一个模式,它可以用于许多目的。我们先来看一个简单的栗子:假设我想要拆分一个字符串,分隔符为数量不定的一组空白符(制表符、空格、换行符等)。描述一个或多个空白符的regex是\s+:
import re text = "foo bar\t baz \tqux" print(re.split('\s+', text))
['foo', 'bar', 'baz', 'qux']
调用re.split('\s+', text)时,正则表达式会先编译,然后再在text上调用其split方法。你可以用re.compile自己编译regex以得到一个可重用的regex对象:
regex = re.compile('\s+') print(regex.split(text))
['foo', 'bar', 'baz', 'qux']
如果只希望得到匹配regex的所有模式,则可以使用findall方法:
print(regex.findall(text))
[' ', '\t ', ' \t']
如果想避免正则表达式中不需要的转义(\),则可以使用原始字符串字面量如r'C:\x'(也可以编写其等价式'C:\x)。
如果打算对许多字符串应用同一条正则表达式,强烈建议通过re.compile创建regex对象。这样将可以节省大量的CPU时间。
match和search跟findall功能类似。findall返回的是字符串中所有的匹配项,而search则只返回第一个匹配项。match更加严格,它只匹配字符串的首部。来看一个小栗子,假设我们有一段文本以及一条能够识别大部分电子邮件地址的正则表达式:
text = """ Dave dave@google.com Steve steve@gmail.com Rob rob@gmail.com Ryan ryan@yahoo.com """ pattern = r'[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}' regex = re.compile(pattern, flags=re.IGNORECASE) print(regex.findall(text))
['dave@google.com', 'steve@gmail.com', 'rob@gmail.com', 'ryan@yahoo.com']
search返回的是文本中第一个电子邮件地址(以特殊的匹配项对象形式返回)。对于上面那个regex,匹配项对象只能告诉我们模式在原字符串中的起始和结束位置:
m = regex.search(text) print(m)
<_sre.SRE_Match object; span=(10, 25), match='dave@google.com'>
print(text[m.start():m.end()])
dave@google.com
regex.match则将返回None,因为它只匹配出现在字符串开头的模式:
print(regex.match(text))
None
相关的,sub方法可以将匹配到的模式替换为指定字符串,并返回所得到的新字符串:
print(regex.sub('REDACTED', text))
Dave REDACTED Steve REDACTED Rob REDACTED Ryan REDACTED
假设你不仅想要找出电子邮件地址,还想将各个地址分成三部分:用户名、域名以及域后缀。要实现此功能,只需将待分段的模式的各部分用圆括号包起来即可:
pattern = r'([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\.([A-Z]{2,4})' regex = re.compile(pattern, flags=re.IGNORECASE)
由这种修改过的正则表达式所产生的匹配项对象,可以通过其groups方法返回一个由模式各段组成的元组:
m = regex.match('wesm@bright.net') print(m.groups())
('wesm', 'bright', 'net')
对于带有分组功能的模式,findall会返回一个元组列表:
print(regex.findall(text))
[('dave', 'google', 'com'), ('steve', 'gmail', 'com'), ('rob', 'gmail', 'com'), ('ryan', 'yahoo', 'com')]
sub还能通过诸如\1、\2之类的特殊符号访问各匹配项中的分组。符号\1对应第一个匹配的组,\2对应第二个匹配的组,以此类推:
print(regex.sub(r'Username:\1, Domain:\2, Suffix:\3', text))
Dave Username:dave, Domain:google, Suffix:com Steve Username:steve, Domain:gmail, Suffix:com Rob Username:rob, Domain:gmail, Suffix:com Ryan Username:ryan, Domain:yahoo, Suffix:com
Python中有许多的正则表达式,但大部分都超出了本书的范围。表是一个简要的概括:
方法 | 说明 |
findall、finditer | 返回字符串中所有的非重叠匹配模式。findall返回的是由所有模式组成的列表,而finditer则通过一个迭代器逐个返回 |
match | 从字符串起始位置匹配模式,还可以对模式各部分进行分组。如果匹配到模式,则返回一个匹配项对象,否则返回None |
search | 扫描整个字符串以匹配模式。如果找到则返回一个匹配项对象。跟match不同,其匹配项可以位于字符串的任意位置,而不仅仅是起始处 |
split | 根据找到的模式将字符串拆分为数段 |
sub、subn | 将字符串中所有的(sub)或前n个(subn)模式替换为指定表达式。在替换字符串中可以通过\1、\2等符号表示各分组项。 |