接下来的4章,会介绍数据分析的核心工作,包括数据清洗、业务模型、数据分析和数据可视化。
不管数据来自外部采集,还是内部系统导出,都会出现不规整的部分,比如不完整、逻辑矛盾、重复、异常等。在正式分析数据前,需要提前处理这些不规整数据,否则会极大影响数据分析的结果。
这部分工作,俗称“数据清洗”,主要任务包括:

  • 基本数据处理:修改列名、时间格式、数字格式、字符串去空格。
  • 删除无效数据:如空数据,或显示为类似”N/A”的数据,或其他无效数据。
  • 剔除无关数据:只选取所需要的数据列,其他的忽略。
  • 删除重复数据:剔除完全一样的数据。
  • 缺失数据处理:用平均值替代,或按特征值处理,如负数或零等。
  • 异常数据处理:删除、按缺失处理、其他具体情况处理。

数据清理工作占整体数据分析工作的80%以上,任务繁琐但至关重要。

本章内容安排如下:

  1. 介绍Python数据分析两大利器:numpypandas
  2. 常用数据操作:索引切片、筛选过滤、统计函数、缺失和字符串处理等。
  3. 演示数据清理环节关键任务的实战操作。

    NumPy基本用法

    在本系列第一篇文章中,我们已经对比过numpy和Python内置数组的性能差异。
    那么关于数据分析,它有哪些实用方法?
    numpy最大的特点就是基于整个数组运算,无需用for循环遍历数组中的每个元素。
    比如,想把一个数组中每个元素都乘以某个整数:
    1. import numpy as np
    2. arr = [1, 2, 3]
    3. # 用numpy的ndarray做乘法
    4. nparr = np.array(arr) * 10
    5. print(nparr)
    6. # 转为list
    7. print(list(nparr))
    8. # 用Python内置数组
    9. arr10 = [a*10 for a in arr]
    10. print(arr10)
    11. # 两个矩阵相加
    12. print(nparr + nparr)
    相比之下,numpy对数组操作更方便。

    广播机制

    numpy在处理两个不同维度矩阵时,如果满足2个原则,会触发广播机制,把低维矩阵与高维矩阵中的各“数据块”进行计算。
    比如,一个1维数组和2维数组相加,2维数组的每行都与1维数组元素相加,结果是2维数组。
    1. import numpy as np
    2. arr_low = np.array([1,2,3])
    3. arr_high = np.array([[1,2,3],[4,5,6]])
    4. # (3,) (2, 3)
    5. print(arr_low.shape, arr_high.shape)
    6. arr_result = arr_low + arr_high
    7. print(arr_result)
    8. # [[2 4 6]
    9. # [5 7 9]]
    成功触发广播机制的2个原则:
  • 维度少的矩阵,其各维度数据长度从后往前与维度多的矩阵能完全匹配上。
  • 当某维度数据长度不一致时,其中一方长度为1,仍视为匹配。

可以通过数据的shape属性查看各维度长度,比如:

  • (2, 3, 4)和(3,4)能匹配
  • (2,3,4)和(1,4)能匹配
  • (2,3,4)和(2,1,4)能匹配
  • (2,3,4)和(3,3,4)不能匹配

“广播”是numpy最基础的关键功能之一。

统计函数

基础系列中,我们用summinmax等内置函数对数组计算,比如:

  • 计算数组总和:sum([1,2,3])
  • 找数组最大值:max([1,2,3])
  • 找数组最小值:min([1,2,3])

对于一维数组,Python内置函数够用,如果是2维数组,用列表生成式也能处理:
[sum(x) for x in [[1,2,3], [4,5,6], [7,8,9]]]
如果维度不断增加,3维、4维、5维……就没那么简单了。
但对于numpy而言,不管多少维,都可以轻松应对。

  1. import numpy as np
  2. # 生成一个3x4的矩阵
  3. data = np.arange(12).reshape(3,4)
  4. # 显示数据维度以及各维长度
  5. print(data.ndim, data.shape)
  6. # 按第1个维度迭代,各行内元素按位相加,结果长度4
  7. print(data.sum(axis=0))
  8. # 按第2维度迭代,各列内元素按位相加,结果长度3
  9. print(data.sum(axis=1))
  10. # 更高维度矩阵
  11. data_3x = np.arange(24).reshape(2,3,4)
  12. print(data_3x)
  13. # 输出其维度
  14. print(data_3x.ndim, data_3x.shape)
  15. # 根据每个维度迭代累加数据
  16. # 累加结果和原数据相比少了一维
  17. for i in range(data_3x.ndim):
  18. print(data_3x.sum(axis=i))

其中包含了几个常见的numpy用法:

  • numpy中最基本的类是ndarray,比如上面的data对象,它能表达多维矩阵。
  • arrange,用来生成序列,和Python的range相似。
  • reshape,用来调整矩阵形状,比如3行x4列
  • ndimshape属性分别代表维度(也叫轴),以及各维度上的数据长度。
  • sum方法能直接累加所有数据,如果指定axis参数则在某维度累加,结果比原数据降一维。

除了sum,还有一系列类似的统计函数,方便对矩阵数据统计:

  1. import numpy as np
  2. # 随机生成4x4的矩阵,元素值范围[1,100)
  3. data = np.random.randint(1, 100, size=(3,4))
  4. print(data)
  5. # 查找最小元素
  6. print(f'最小值:{np.amin(data)}')
  7. # 按第2维度查找每行最小值
  8. print(f'每行最小值:{np.amin(data, 1)}')
  9. # 按第1维度查找每列最小值
  10. print(f'每列最小值:{np.amin(data, 0)}')
  11. # 查找中位数
  12. print(f'中位数:{np.median(data)}')
  13. # 查找算数平均值
  14. print(f'算数平均值:{data.mean()}')
  15. # 加权平均值 average=sum(x*wt)/sum(wt)
  16. wt = np.array([1, 2, 3, 4])
  17. print(f'加权平均值:{np.average(data, axis=1, weights=wt)}')
  18. # 方差 var=mean((x - x.mean())**2)
  19. print(f'方差:{np.var([1,2,3,4])}')
  20. # 标准差 std=sqrt(var)
  21. print(f'标准差:{np.std([1,2,3,4])}')

索引和切片

和Python中list一样,numpy数组也支持索引和切片访问数据。

  1. import numpy as np
  2. arr = np.arange(10)
  3. print(f'1维数组:{arr}')
  4. # 单值索引
  5. print(f'单值索引:{arr[3]}')
  6. # 多值索引
  7. print(f'多值索引:{arr[[1, 2, 4]]}')
  8. # [3, 8),步进2
  9. s = slice(3,8,2)
  10. print(f'切片索引:{arr[s]}')
  11. # 等价于
  12. print(f'等价于:{arr[3:8:2]}')
  13. # 布尔索引
  14. print(f'找出大于3的元素:{arr[arr>3]}')
  15. # 数组索引,用数组值当下标索引
  16. # 与切片不同,会把结果复制到新数组
  17. print(f'数组索引:{arr[[7, 5, 7]]}')
  18. # 2维矩阵索引
  19. arr2 = np.arange(20).reshape(4,5)
  20. print(f'2维矩阵{arr2}')
  21. # 多维索引:第3、4行,第4、5列
  22. print(f'多维索引:{arr2[2:4, 3:5]}')
  23. # 获取(0,1)和(2,3)两个坐标元素
  24. print(f'获取坐标{arr2[[0, 2], [1,3]]}')
  25. # 数组索引,用数组值当行下标
  26. print(f'2维矩阵数组索引:{arr2[[3,0, 1]]}')
  27. # 第4列后数据
  28. print(f'第4列后:{arr2[:, 3:]}')
  29. # 看上去省略号和纯冒号效果一样,其实不同
  30. print(f'省略号效果:{arr2[..., 3:]}')
  31. # 3维矩阵看省略号和冒号不同
  32. arr3 = np.arange(24).reshape(2,3,4)
  33. print(f'3维矩阵:{arr3}')
  34. print('省略号用法')
  35. print(arr3[..., 3:])
  36. print('冒号用法')
  37. print(arr3[:, :,3:])

在数据分析中,我们经常会用布尔索引清理数据,比如剔除一些无效数据:

  1. import numpy as np
  2. # np.nan一般表示缺失元素
  3. arr = np.array([1, 2, np.nan, 3])
  4. # 剔除非数字元素
  5. print(arr[~np.isnan(arr)])

排序和筛选

  1. import numpy as np
  2. arr = np.random.randint(1, 100, (3,4))
  3. print(f'原数据:{arr}')
  4. # 默认按最后轴用quicksort算法排序
  5. # 用np.sort会生成副本,不改变原数据
  6. print(f'np.sort生成副本:{np.sort(arr)}')
  7. # axis=1按行排序
  8. arr.sort()
  9. print(f'就地排序后:{arr}')
  10. # axis=0按列排序
  11. arr.sort(axis=0)
  12. print(arr)
  13. # 筛选元素,返回坐标序列
  14. x, y = np.where(arr>60)
  15. # 根据坐标索引元素
  16. print(f'条件筛选: {arr[x, y]}')
  17. # 多个条件的逻辑组合
  18. x, y = np.where((arr>60) & (arr<80))
  19. print(f'组合条件筛选:{arr[x,y]}')
  20. # 用where实现类似Python的三元操作
  21. a = np.array([1,2,3])
  22. b = np.array([4,5,6])
  23. cond = np.array([True, False, True])
  24. print(f'where三元操作:{np.where(cond, a, b)}')
  25. # 等同于这样的Python实现
  26. result = [(ia if ic else ib) for ia, ib, ic in zip(a, b, cond)]
  27. print(f'同等Python实现:{result}')
  28. # 根据条件筛选元素,如挑选偶数
  29. print(f'布尔矩阵:{arr%2==0}') # 生成布尔矩阵
  30. # 根据布尔值抽取元素
  31. print(f'偶数:{np.extract(arr%2==0, arr)}')
  32. # 去重复元素
  33. data = np.array([1,2,3,1,2,3])
  34. print(np.unique(data))

其中where的条件筛选中,逻辑运算可以用&(并且)、|(或者)等运算符组合。

字符串

前面的功能介绍主要集中在数字类型功能上,numpy也支持其他数据类型,如时间、字符串等。
可以通过ndarray.dtype查看数据类型,如np.int64np.int8np.float64等。
字符串的数据类型对应numpy.string_numpy.unicode_

  1. import numpy as np
  2. arr = np.array(['hello', 'world3'])
  3. isinstance(arr[0],np.unicode_) # True

常用操作包括:大小写转换、查找替换、前后去空格、分割合并等。

  1. import numpy as np
  2. arr = np.array(['Python', ' 1,024 ', '程一初', '只差一个程序员了', '数据分析系列'])
  3. print(f'原数据{arr}')
  4. # 大小写转换
  5. print(f'大写:{np.char.upper(arr)}')
  6. print(f'小写:{np.char.lower(arr)}')
  7. # 查找替换
  8. print(f'查找:{np.char.find(arr, "程序")}')
  9. print(f'替换:{np.char.replace(arr, "分析", "处理")}')
  10. # 前后去空格
  11. print(f'前后去空格:{np.char.strip(arr)}')
  12. # 分割合并
  13. sep = np.char.split(arr, ",")
  14. print(f'分割:{sep}')
  15. print(f'合并:{np.char.join("-", sep)}')

相比Python列表,numpy大部分操作都是直接修改原数据,不产生副本,达到高性能省空间效果。
常见产生副本的场景主要有4类,可以通过id函数验证:

  • np.array,让它接受一个ndarray作为参数时,会产生副本np.asarray则不产生副本。
  • 数组索引,会生成副本。
  • ndarray.copy方法可以主动生成副本。
  • 调用np顶层方法,比如用np.sortnp.char,会返回数据副本。

当然,numpy还支持更多线性代数和矩阵运算,建议先掌握上述最常用功能,其他需要时查找即可。

Pandas基本用法

pandas基于numpy构建,使用风格很相似,此外增强了表格数据处理能力。
如果把numpy看成是Python内置list的升级版,那么pandas就是dict的升级版,它支持更复杂数据类型,以及更丰富的数据顺序。
pandas中最主要的两个数据结构:SeriesDataFrame
可以借助inspect标准模块查看它们的继承结构:

  1. import inspect
  2. import pandas as pd
  3. s = pd.Series([1, 2, 3])
  4. print(inspect.getmro(type(s.index)))
  5. print(inspect.getmro(type(s.values)))
  6. print(inspect.getmro(pd.Series))
  7. print(inspect.getmro(pd.DataFrame))

Series基本使用

Series是加了索引的1维数组,就像dictkey是索引,valuendarray

  1. import pandas as pd
  2. s = pd.Series([1, 2, 3])
  3. print(s.index, s.values)
  4. s2 = pd.Series([4,5,6], index=['x', 'y', 'z'])
  5. print(s2.index, s2.values)

Series默认使用从0开始的整数序列作为索引,也能指定索引。
访问数据时,既可以用dict形式也可以用numpy形式:

  1. import pandas as pd
  2. # 默认用0开始的整数序列作为index
  3. s = pd.Series([1, 2, 3])
  4. print(s.index, s.values)
  5. # 指定index
  6. s2 = pd.Series([4,6,8,5], index=['x', 'y', 'z','a'])
  7. print(s2.index, s2.values)
  8. # 从dict创建,key即索引
  9. s3 = pd.Series({'x':4,'y':6,'z':8,'a':5})
  10. print(s3)
  11. print(f'比较s3和s2\n{s3==s2}') # 每个元素都相等
  12. print(f'但非同一对象:{id(s2)==id(s3)}') # False
  13. # dict式访问数据
  14. print(s[0], s2['x'])
  15. # 判断index是否在series
  16. print('x' in s2) # True
  17. # numpy式访问数据
  18. print(s2[1:]) # 5,6
  19. print(s2[s2>4]) # 5,6
  20. print(s2[(s2>4) & (s2<6)]) # 5
  21. # 排序
  22. print(f'按index排序:\n{s2.sort_index()}')
  23. print(f'按value排序:\n{s2.sort_values()}')

当两个Series执行计算,如相加,pandas会按索引对齐数据。

  1. import pandas as pd
  2. s1 = pd.Series([4,6,8], index=['x', 'y', 'z'])
  3. s2 = pd.Series([1,3,5,7,9], index=['a','x','y','b', 'c'])
  4. print(f's1+s2:\n{s1 + s2}')

注意,由于s2没有z索引,所以结果里z索引值也为NaN(Not a Number),pandas会用NaN表示缺失数据。
可以用isnullnotnull判断缺失数据,处理时可以丢弃也可以填充。

  1. import pandas as pd
  2. s = pd.Series([1,2,None,4,4])
  3. print(s)
  4. print(f'isnull判断:\n{s.isnull()}')
  5. print(f'notnull判断:\n{s.notnull()}')
  6. print(f'过滤缺失值1:\n{s[~s.isnull()]}')
  7. print(f'过滤缺失值2:\n{s[s.notnull()]}')
  8. # 填充或删除操作默认会产生副本,可用inplace=True参数就地修改原数据
  9. print(f'用0填充缺失值:\n{s.fillna(value=0)}')
  10. print(f'向前填充缺失值:\n{s.fillna(method = "ffill")}')
  11. print(f'向后填充缺失值:\n{s.fillna(method = "bfill")}')
  12. # 删除缺失值
  13. print(f'删除缺失值:\n{s.dropna()}')
  14. # 删除重复值
  15. print(f'删除重复:\n{s.drop_duplicates()}')

DataFrame基本使用

DataFrame是一种表格型数据结构,行和列都有索引,可以看成多个Series共享了一套索引。
你可以简单把它理解成升级版2维Excel表,但其实它能用层次化索引处理更高维数据。
访问DataFrame内数据主要有4种形式:

  • dict一样用关键词索引;
  • numpy一样切片、布尔索引和数组索引;
  • 通过lociloc属性,按标签名和下标切片索引;
  • 通过`atiat属性,直接获取某个坐标值。
    1. import numpy as np
    2. import pandas as pd
    3. data = {'文章ID': ['WX10001', 'WX10002', 'WX10003', 'WX10004'],
    4. '标题': ['标题1', '标题2', '标题3', '标题4'],
    5. '阅读量': [1000, 800, 1400, 800]}
    6. # 从dict创建,默认索引是从0开始的整数序列
    7. df = pd.DataFrame(data)
    8. print(df)
    9. print(f'所有列:{df.columns}')
    10. print(f'行和列都有索引:{type(df.columns), type(df.index)}')
    11. print(f'值是2维ndarray:{type(df.values), df.values.shape}')
    12. # 指定列及其顺序,指定index
    13. df = pd.DataFrame(data, columns=['标题', '文章ID','阅读量','点赞量'],
    14. index=['a','b','c','d'])
    15. print(df) # "点赞量"列值NaN
    16. # 列索引,用dict或属性操作形式访问
    17. print(df['标题'] == df.标题)
    18. # 用dict形式判断是否在列索引中
    19. print(f'判断是否在列中:{"阅读量" in df.columns}')
    20. # 每个列是一个Series
    21. print(f'列的数据类型:{type(df["标题"])}')
    22. # 整列赋值
    23. df['点赞量'] = np.random.randint(100,1000,4)
    24. print(df)
    25. df['在看数'] = pd.Series([100,200,300], index=['a','b','c'])
    26. df['是否爆文'] = df['阅读量'] >=1000
    27. print(df)
    28. # 删除列
    29. del df['是否爆文']
    30. print(df)
    31. print(f'drop列:{df.drop("标题", axis=1)}')
    32. # 删除行,drop默认会生成副本
    33. # inplace参数可以就地修改原数据
    34. print(f'drop行:{df.drop("a")}')
    35. # 行索引
    36. print(f'切片行索引:\n{df[2:4]}')
    37. # 按下标索引,效果相同,但更通用
    38. print(f'iloc行索引:\n{df.iloc[2:4]}')
    39. # 行列索引,支持切片和数组索引混合
    40. print(f'iloc行列索引:\n{df.iloc[2:4,[0,2,3]]}')
    41. print(f'iloc数组索引:\n{df.iloc[[2,3],[2,3]]}')
    42. print(f'loc行索引:\n{df.loc["a"]}')
    43. # 按标签索引,标签切片包含两端[a,b],而非[a,b)
    44. print(f'loc行索引:\n{df.loc["a":"b",["标题","阅读量"]]}')
    45. # 访问某个值
    46. print(f'at访问:{df.at["a","标题"]}')
    47. print(f'iat访问:{df.iat[0,2]}')
    48. # 扩充和缩减索引
    49. print(f'缩减行和列:\n{df.reindex(index=["a","b"],columns=["标题","阅读量"])}')
    50. print(f'扩大行和列:\n{df.reindex(index=["d","e"],columns=["标题","新增列"])}')
    51. print(f'自动填充:\n{df.reindex(index=["d","e"],columns=["阅读量"], fill_value=0)}')

实战中,我们可以先选出需要的列,然后用条件筛选数据。```python import pandas as pd df = pd.DataFrame(np.arange(12).reshape(3,4), index=list(‘XYZ’), columns=list(‘ABCD’)) print(df)

布尔筛选

print(f’大于5的元素:\n{df[df>5]}’) print(f’A大于5的元素:\n{df[df.A>5]}’)

isin筛选出元素范围

print(f’A列值匹配选项:\n{df[df.A.isin([4,5,6])]}’)

组合筛选

print(f’A大于5或B大于4的C和D列:\n{df[[“C”,”D”]][(df.A>5)|(df.B>4)]}’)

  1. `DataFrame`间可进行算数计算,与`Series`间计算也适用“广播原则”。```python
  2. import pandas as pd
  3. df = pd.DataFrame(np.arange(12).reshape(3,4),
  4. index=list('XYZ'), columns=list('ABCD'))
  5. print(f'两个形状相同的df相加:\n{df + df}')
  6. # df和series相加,默认按行相加
  7. print(f'df和同形series相加:\n{df + df.iloc[0]}')
  8. # 如果series形状相同,但列标签不同,结果是索引并集,填充NaN
  9. s = pd.Series(range(4), index=list('BCDE'))
  10. print(f'df和通型不同index的Series相加:\n{df + s}')
  11. # df和series按列相加
  12. s2 = pd.Series(range(3), index=list('XYZ'))
  13. print(f'df和series按列相加:\n{df.add(s2, axis="index")}')

numpy的顶级函数可直接作用于DataFrame,此外可以通过applyapplymap两个方法分别作用于行列和单个元素。```python import numpy as np import pandas as pd df = pd.DataFrame(np.random.randint(1,100,(3,4)), index=list(‘XYZ’), columns=list(‘ABCD’)) print(df)

对每列统计

print(df.describe())

统计方法默认对行操作得出每一列结果汇总

print(f’每列算数平均值:\n{df.mean()}’) print(f’每行求和:\n{df.sum(axis=”columns”)}’) print(f’每列累加:\n{df.cumsum()}’) print(f’每列累积:\n{df.cumprod()}’)

统计某一列各值出现次数

print(f’A列各值频率:\n{df[“A”].value_counts()}’)

列元素去重

print(f’A列元素去重:\n{df[“A”].unique()}’)

numpy的全局函数可以应用在dataframe

np.sort默认就地修改原数据

print(f’按行排序\n{np.sort(df)}’)

sort_values保持数据间关联

print(f’按某列排序\n{df.sort_values(by=”A”)}’)

sort_index保持数据关联

print(f’按行索引排序\n{df.sort_index(ascending=False)}’) print(f’按列索引排序\n{df.sort_index(ascending=False,axis=1)}’)

对每行/列应用函数,如计算最大最小差

f = lambda x: x.max() - x.min()

默认按行计算,得出每列结果

print(f’每列最大最小差:\n{df.apply(f)}’)

按列计算,得出每行结果

print(f’每行最大最小差:\n{df.apply(f,axis=”columns”)}’)

describe的简单实现

f = lambda x: pd.Series([x.min(), x.max(),x.mean(),x.std(),x.count()], index=[‘min’,’max’,’mean’,’std’,’count’]) print(f’简单的describe:{df.apply(f)}’)

元素级映射

fmt = lambda x: f’{x:.2f}’ print(f’2位小数:\n{df.applymap(fmt)}’)

单列元素级映射

print(f’单列格式化2位小数:\n{df[“A”].map(fmt)}’)

  1. `pandas`中对字符串的操作,通过`Series.str`操作,基本与Python内建`str`方法同名。```python
  2. import pandas as pd
  3. s = pd.Series(['Python','1,024','程一初','只差一个程序员了','数据分析系列'])
  4. print(f'原数据\n{s}')
  5. # 大小写转换
  6. print(f'大写:\n{s.str.upper()}')
  7. print(f'小写:\n{s.str.lower()}')
  8. # 查找替换
  9. print(f'查找:\n{s.str.find("程序")}')
  10. print(f'替换:\n{s.str.replace("分析", "处理")}')
  11. # 前后去空格
  12. print(f'前后去空格:\n{s.str.strip()}')
  13. # 分割合并
  14. sep = s.str.split(",")
  15. print(f'分割:\n{sep}')
  16. print(f'合并:\n{sep.str.join("-")}')
  17. # 连接各元素
  18. print(f'合并所有元素:\n{s.str.cat(sep=",")}')
  19. # 根据是否包含字符串生成布尔矩阵
  20. print(f'根据包含字符串生成布尔矩阵:\n{s.str.contains("程序")}')

pandas中大部分的操作都不会修改原数据,而是生成副本,需要就地修改原数据时可以添加inplace参数。
上面是pandas的基础功能和常见用法,包括:索引切片访问、筛选过滤、统计函数、缺失和字符串处理等,这些功能会贯穿数据分析的整个过程,需要熟练掌握。
更多分析功能会放在数据分析章节中介绍,比如:合并(Merge)、分组(Group by)、重塑(Reshaping)、透视表(Pivot Table)、时间序列(TimeSeries)、分类(Categorical)等。

数据清理实战操作

Python处理数据的第1步,是把数据装入内存。
pandas提供了多种主要数据格式读写功能,包括:CSV、JSON、HTML、Excel、HDF、SQL、SAS等,甚至还支持本地剪切板。
负责对应文件格式读写的函数分别由read_to_开头,比如:

  • CSV格式读写:read_csvto_csv
  • HTML格式读写:read_htmlto_html
  • 以此类推。

比如我们从CSV文件读取数据,然后快速看看前10行的模样:

  1. import pathlib
  2. import pandas as pd
  3. path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/004preproc')
  4. csv_path = path.joinpath('test.csv')
  5. df = pd.read_csv(csv_path)
  6. df.head(10)

4、数据清洗 - 图1
可以看到,数据并不规整,比较杂乱。
一般而言,数据源越少、导出数据的系统越成熟,数据就会相对规整。
但,大部分情况下,我们都会面临历史遗留数据,经常会出现这些不规整情况:

  • 数据结构不一致,有些字段在后期运营过程中加入,之前的数据就会出现空值;
  • 数据格式不一致,如开始用户编号没设计好,达到极限后重新设计编码,导致前后格式不一致;
  • 部分数据由用户直接输入,会出现如空格、格式符号、全/半角、中英文标点等数据;
  • 有些数据导出自不同表格,会出现重复现象,甚至逻辑矛盾不一致;
  • 有些系统数据包含多国语言或多国货币符号;
  • ……还有无数可能出现情况……

总之:数据不规整,是常态,如果遇到像上面这样的,值得庆幸。

那该如何清理数据?有什么标准流程?
其实,数据清理工作,并没有统一标准,不同业务、不同经验的人,处理同一份数据可能都会得到不一样的结果。
但面对常见的场景,行业内有一些通用方法和原则

  1. 打开数据后,先直观感受,尝试从业务角度理解数据,比如有多少字段、每个字段什么意思、字段间是否有关联等。
  2. 明确分析的目标,找到目标相关的字段,可以另行标注记录方便查找。
  3. 观察相关数据不规整的具体情况,建议列出清单,方便自己后续逐项检查,如某列大小写不一致、某列存在缺失数据、某列数字格式不一致等。
  4. 根据清单逐项检查不规整情况,用Python处理完毕。
  5. 保存预处理后的数据,如果数据量大,建议过程中保存多个副本,对应清单某项或某几项的不规整处理结果。

从数据前几行以及表头看,案例数据是一份电商交易数据。
我们可以先调用df.info()df.describe()等方法查看数据分布。
4、数据清洗 - 图2
运行后发现上面案例数据里有几个潜在不规整现象:

  1. discount列和comment列存在缺失数据;
  2. datetime列数据类型为object,后续尝试转换为日期类型;
  3. discount列数据应为数字,但却是object类型,说明有数据格式不一致。

先不着急处理这些问题,我们检查下每一列数据,看看各个值的出现频率。
在统计频率前,我们先把每列的大小写转换好,不然会因为大小写影响统计结果。
此外,可以把表头换成更容易理解的名字,降低阅读门槛。

  1. colname_dict = {'orderid':'订单ID', 'datetime':'时间日期',
  2. 'userid':'用户ID','productid':'产品ID',
  3. 'price':'原价','discount':'折扣率','pay':'实付金额',
  4. 'paymode':'付款方式','comment':'订单留言'}
  5. # 重命名列名称
  6. df.rename(columns=colname_dict, inplace=True)
  7. # '订单ID'列有数据缺失,如果直接转大小写,会提示float对象无upper方法
  8. # 因为缺失数据用NaN代表,它是float类型。
  9. # 用isnull生成布尔矩阵,用any判断是否存在True
  10. print(df['订单ID'].isnull().any())
  11. # 这里订单ID不影响统计,可指定缺省值,如MISSED
  12. df['订单ID'].fillna('MISSED', inplace=True)
  13. # ID类信息,全部转为大写模式,去前后空格
  14. df[['订单ID','用户ID','产品ID']] = df[['订单ID','用户ID','产品ID']].applymap(lambda x : x.upper().strip())

注意:在大小写转换等过程中,如果发生异常,也可能意味着部分列中存在数据缺失或格式错误,如案例中“订单ID”列。
经过基本转换后,数据更容易观察了:
4、数据清洗 - 图3

然后,我们可以观察每列数据频率:

  1. for n, s in df.items():
  2. # 列名称和对应列Series
  3. print(f'----{n}----')
  4. print(s.value_counts())

从输出中,可以进一步看到数据不规整的现象,比如订单ID有重复、折扣率里有中文格式。
4、数据清洗 - 图4
也可以从中看到一些基本统计信息,进一步帮助理解数据,比如:

  • 哪些用户下单频率更高?
  • 哪些产品订单更多?
  • 常见客户留言是什么?
  • 有哪些支付渠道更多?

很多时候,这些看似简单的信息,能在分析过程中起到关键作用。

接下来,就可以开始逐项处理清单内的任务,比如:

  • 订单去重
  • 日期时间格式转换
  • 折扣率转为数字格式
  • 合并部分付款方式,如微信
    1. # 订单去重
    2. # 默认保留第一次出现的项,修改原数据
    3. df.drop_duplicates('订单ID', inplace=True)
    4. # 日期时间格式转换
    5. # 通常情况下,用自动推断日期格式,或指定某个格式format='%Y-%m-%d'即可
    6. # df['时间日期'] = pd.to_datetime(df['时间日期'], infer_datetime_format=True)
    7. # 但如果遇到多种格式数据,就得分情况匹配
    8. def to_date(x):
    9. import dateutil
    10. try:
    11. result = pd.to_datetime(x, infer_datetime_format=True)
    12. except dateutil.parser.ParserError:
    13. try:
    14. result = pd.to_datetime(x, format='%Y年%m月%d日')
    15. except dateutil.parser.ParserError as e:
    16. print('日期格式解析失败', e)
    17. return result
    18. df['时间日期'] = df['时间日期'].map(to_date)
    19. # 用实付金额/原价,填补折扣率缺失值
    20. disc_f = lambda x: x['实付金额']/x['原价']
    21. df_cond = df['折扣率'].isnull() # 布尔矩阵
    22. # 计算缺失值,返回Series
    23. s_disc = df[df_cond].apply(disc_f,axis='columns')
    24. # 为Series设置名字后,就可以用update方法就地更新数据
    25. s_disc.name = '折扣率'
    26. df.update(s_disc)
    27. # 折扣率转为数字格式
    28. df_cond_chn = df['折扣率'].str.contains('折').fillna(False)
    29. disc_chn_f = lambda x: float(x.replace('折',''))/10
    30. s_disc_chn = df[df_cond_chn]['折扣率'].map(disc_chn_f)
    31. s_disc_chn.name = '折扣率'
    32. df.update(s_disc_chn)
    33. # 合并微信付款方式
    34. paym_cond = df['付款方式'].str.contains('微信-')
    35. s_paym = df[paym_cond]['付款方式'].map(lambda x: x.split('-')[0])
    36. s_paym.name = '付款方式'
    37. df.update(s_paym)

数据清理后,变得规整了: 4、数据清洗 - 图5

总结

本文介绍了numpypandas两大数据利器的基本使用,包括索引切片、筛选过滤、统计函数、缺失和字符串处理等常见操作。
同时通过案例数据,演示了实战中数据清理的思路、通用方法,以及常见操作。
数据清理完毕后,就可以进入数据分析环节。
实战中,数据往往会更复杂,需要耐心多尝试一些方法,比如:

  • 看多几行数据,比如随机排序或采样抽检数据
  • 多观察列和列之间的关系
  • 对每个列多做些测试或验证,比如排序、排名、筛选等
  • 对于无关分析结果的数据,可以直接按丢弃处理
  • 对于部分缺失数据,可用中位数、平均数等填充

扫码加入学习群,前100名免费。
4、数据清洗 - 图6