7.《Python数据分析》数据清洗和准备

在整个数据分析和建模的过程中,数据清洗和预处理将占据其中80%的时间

1 处理缺失数据

pandas沿用了R语言中的习惯,将缺失值表示为NA(not available)

  • Python内置的None也可以作为NA
  • 对于浮点型数据来说,也会用NaN(Not a Number)表示缺失

处理缺失的代码示例:

string_data = pd.Series(["aardvark", np.nan, None, "avocado"])
string_data.isna() # 判断是否为缺失值
# 结果是 False True True False
string_data.dropna() # 直接丢弃存在缺失的数据

data = pd.DataFrame([[1., 6.5, 3.], [1., np.nan, np.nan],
                      [np.nan, np.nan, np.nan], [np.nan, 6.5, 3.]])
data.dropna(how="all") # 当一行的数据全是缺失时才舍弃这一行
data.dropna(axis=1, thresh=2) # 当一列的数据缺失量大于2时才舍弃这一列
data.fillna(0) # 填充缺失为0
data.fillna({1: 0.5, 2: 0}) # 第2列使用0.5填充缺失;第3列使用0填充缺失
data.fillna(method="bfill") # 使用向前填补的方式填充缺失
data.fillna(method="ffill", limit=2) # 使用向后填补的方式填充缺失,最多插值2此
data[0].fillna(data[0].mean()) # 第一列按照均值填充缺失

2 数据转换

处理数据重复的代码示例:

data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'],
                      'k2': [1, 1, 2, 3, 3, 4, 4]})
data.duplicated() # 判断各行是否是重复行
data.drop_duplicates() # 舍弃重复行
data.drop_duplicates(['k1']) # 指定部分列进行重复项判断
data.drop_duplicates(['k1', 'k2'], keep='last') # 出现重复时保留最后一个

常见数据转换的代码示例:

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'
}
# 将字符串处理为小写,并根据字典进行值的映射与转换
data['animal'] = data['food'].str.lower().map(meat_to_animal)
data['animal'] = data['food'].map(lambda x: meat_to_animal[x.lower()]) # 另一种写法
# 使用rename可以对行索引或列索引/列名进行重命名或映射转换
data = data.rename(index=['a1','a2','a3','a4','a5','a6','a7','a8','a9'])
data.rename(index={'a1': 'A1'}, columns={'food': 'Food'}, inplace=True) # 原地修改
# 使用cut将连续型数据转化为离散型数据
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)
cats.codes # 结果是[0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1]
pd.cut(data, 4, precision=2) # 另一种离散化方式,直接指定桶数=4,分桶区间长度是一致的
pd.qcut(data, 4, precision=2) # 根据样本分位数进行离散化,分桶区间长度一般是不一致的

cut函数返回一个Categorical对象,bins对于着数据的分桶,value_counts就是针对分桶的计数;每个桶的区间一般的前闭后开的,上文代码示例对应的分桶区间是[[18, 25) < [25, 35) < [35, 60) < [60, 100)]

异常值的发现与处理:

data[2][np.abs(data[2]) > 3] # 筛选第3列数据中绝对值大于3的
data[np.abs(data) > 3] = np.sign(data) * 3 # 将数据的绝对值限制在3以内
data.replace(-999, np.nan) # 将异常值进行批量替换
data.replace({-999: np.nan, -1000: 0}) # 针对不同异常值进行个性化处理

其他常用的数据转换方法:

  • 使用numpy.random.permutation函数对数据进行随机重排序
  • 使用sample方法在Series和DataFrame上选取随机子集
  • 使用pandas.get_dummies函数对类别型变量进行哑变量处理

3 扩展数据类型

pandas构建在numpy的基础之上,这可能存在一些缺点:

  • 对于数值型缺失,需要用np.nan额外兼容(并且还存在隐患)
  • 当数据集中存在大量字符串时,计算成本高,内存占用大
  • 为了兼容特殊的数据类型(如时间间隔、带时区的时间戳等)需要付出额外的代价

pandas引入了扩展类型(extension type),用来灵活兼容更多地类型

pd.Series([1, 2, 3, None]) # 老版(float64),缺失值用NAN表示
pd.Series([1, 2, 3, None], dtype=pd.Int64Dtype()) # 新版,缺失值用NA表示
pd.Series([1, 2, 3, None], dtype=pd.Int64()) # 新版,类型简写的形式
pd.Series(['one', 'two', None, 'three'], dtype=pd.StringDtype()) # 扩展版字符串

其他常用的扩展类型:BooleanDtypeCategoricalDtypeDatetimeTZDtypeFloat32DtypeFloat64DtypeInt8DtypeInt16DtypeInt32DtypeUInt8DtypeUInt16DtypeUInt32DtypeUInt64Dtype

一般扩展类型会使用首字母大写的形式,以方便与老版的类型区分

基于扩展类型构建的类型(如字符串)一般计算效率更高,内存占用更少

4 字符串操作

字符串中子字符串的查找:

  • 直接使用in关键字,可以判断是否存在相应的子字符串
  • index查找(找不到会报异常,否则返回第一个的起始索引)
  • find查找(找不到会返回-1,否则返回第一个的起始索引)
  • rfind查找(找不到会返回-1,否则返回最后一个的起始索引)

其他常见字符串操作:split切分、strip/rstrip/lstrip去除空白符、join拼接、、count计数、replace替换、lower小写、upper大写、startswith以xxx开始,endswith以xxx结束,ljust/rjust左右对齐

正则表达式相关内容可参阅本人之前的总结:正则表达式

在pandas中,通过Series的str属性可访问字符串的矢量化方法:

data = pd.Series({"Dave": "[email protected]", "Steve": "[email protected]",
         "Rob": "[email protected]", "Wes": np.nan})
data.str.contains("gmail") # 是否包含某些关键字
data.str.findall(pattern, flags=re.IGNORECASE) # 矢量化的正则匹配
data.str.extract(pattern, flags=re.IGNORECASE) # 基于正则的文本抽取

刚刚提到的几个字符串操作基本都是支持矢量化的,除此之外常用的还有:cat拼接字符串、len计算长度、repeat重复、islower/isdigit/isdecimal/isnumeric常见类型的判断、slice切片

5 类别型数据

之前已经提到了两个类别型数据常用的函数:uniquevalue_counts

用整数表示的类别型数据可以借助take方法转化为和文本类别型:

values = pd.Series([0, 1, 0, 0])
dim = pd.Series(['apple', 'orange'])
dim.take(values)
# 结果是apple orange apple apple

类别型数据一般存在大量的重复值,扩展类型Categorical针对此现象进行了数据压缩,大幅减少了类别型数据的内存占用和计算成本(特别是针对文本类别型):

# 定义扩展类型Categorical的两种方式
my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
my_categories = pd.Series(['foo', 'bar', 'baz', 'foo', 'bar']).astype(Categorical)
# Categorical类型数据包含两部分:类型与编码后的数据
my_categories.categories, my_categories.codes
# Categorical类型可以直接借助类型,将编码后的数据转换回去
categories = ['foo', 'bar', 'baz']
codes = [0, 1, 2, 0, 0, 1]
pd.Categorical.from_codes(codes, categories)
# 对比Categorical类型与普通类别型的内存占用
labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (10_000_000 // 4))
labels.memory_usage(deep=True) # 结果是600000128
categories = labels.astype('category')
categories.memory_usage(deep=True) # 结果是10000540
# 对比Categorical类型与普通类别型的计算占用
%timeit labels.value_counts() # 840 ms +- 10.9 ms per loop
%timeit categories.value_counts() # 30.1 ms +- 549 us per loop

除了整数和字符串,类别型变量的值也可以是其他不可变类型的值

类别型数据的方法示例:

cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2).astype('category')
cat_s = cat_s.cat.set_categories(['a', 'b', 'c', 'd', 'e']) # 修正类别的总数目
cat_s.value_counts() # 分类计数,其中e的计数是0
cat_s.cat.remove_unused_categories() # 剔除未在数据中出现的类别

其他类别型数据的常用方法:add_categories增加类别、as_ordered含次序的类别、as_unordered不含次序的类别、remove_categories删除类别、rename_categories类别重命名、set_categories重置类别、get_dummies对类别进行哑变量处理

往年同期文章