|
|
马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
引言
在数据科学和分析领域,数据清洗和预处理是确保分析结果准确可靠的关键步骤。NumPy作为Python中用于科学计算的核心库,提供了强大的多维数组对象和丰富的函数集,使其成为数据清洗和预处理的理想工具。本指南将全面介绍如何使用NumPy进行高效的数据清洗和预处理,从基础操作到高级技巧,帮助你提升数据分析的质量与准确性。
NumPy的优势在于其高效的数组操作、广播功能和广泛的数学函数库,这些特性使数据处理变得更加简洁和高效。通过掌握NumPy的数据清洗和预处理技术,你将能够更快地准备数据集,为后续的分析和建模工作奠定坚实的基础。
NumPy基础回顾
在深入数据清洗和预处理之前,让我们简要回顾一下NumPy的核心概念和数据结构。
NumPy数组
NumPy的核心是ndarray对象,它是一个快速、灵活的大型数据集容器。与Python列表不同,NumPy数组要求所有元素具有相同的数据类型,这使得数组操作更加高效。
- import numpy as np
- # 创建一维数组
- arr1d = np.array([1, 2, 3, 4, 5])
- print("一维数组:")
- print(arr1d)
- # 创建二维数组
- arr2d = np.array([[1, 2, 3], [4, 5, 6]])
- print("\n二维数组:")
- print(arr2d)
- # 创建全零数组
- zeros_arr = np.zeros((3, 4))
- print("\n全零数组:")
- print(zeros_arr)
- # 创建全一数组
- ones_arr = np.ones((2, 3))
- print("\n全一数组:")
- print(ones_arr)
- # 创建随机数组
- random_arr = np.random.rand(2, 3)
- print("\n随机数组:")
- print(random_arr)
复制代码
数组属性
NumPy数组有几个重要属性,可以帮助我们了解数组的基本信息:
- arr = np.array([[1, 2, 3], [4, 5, 6]])
- print("数组形状:", arr.shape) # 数组的维度
- print("数组维度:", arr.ndim) # 数组的维数
- print("数组大小:", arr.size) # 数组的元素总数
- print("数组类型:", arr.dtype) # 数组的数据类型
复制代码
数组索引和切片
NumPy数组支持多种索引和切片方式,这对于数据清洗和预处理非常重要:
- arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
- # 基本索引
- print("第二行第三列的元素:", arr[1, 2]) # 输出: 7
- # 切片操作
- print("前两行:")
- print(arr[:2, :])
- print("所有行的第二列:")
- print(arr[:, 1])
- # 布尔索引
- bool_idx = arr > 5
- print("大于5的元素:")
- print(arr[bool_idx])
复制代码
掌握了这些基础概念后,我们可以开始探索NumPy在数据清洗和预处理中的应用。
数据导入与导出
在数据清洗和预处理的第一步,我们需要将数据导入到NumPy数组中。NumPy提供了多种方法来加载和保存数据。
从文本文件加载数据
- # 从CSV文件加载数据
- data = np.genfromtxt('data.csv', delimiter=',', skip_header=1)
- print("从CSV加载的数据:")
- print(data)
- # 使用loadtxt函数
- data = np.loadtxt('data.txt', delimiter='\t')
- print("\n从文本文件加载的数据:")
- print(data)
复制代码
从二进制文件加载数据
- # 保存数组到二进制文件
- arr = np.array([[1, 2, 3], [4, 5, 6]])
- np.save('array_data.npy', arr)
- # 从二进制文件加载数据
- loaded_arr = np.load('array_data.npy')
- print("从二进制文件加载的数据:")
- print(loaded_arr)
复制代码
处理不同格式的数据
- # 处理包含缺失值的数据
- data_with_missing = np.genfromtxt('data_with_missing.csv', delimiter=',',
- missing_values='', filling_values=0)
- print("处理缺失值后的数据:")
- print(data_with_missing)
- # 指定数据类型
- data_with_types = np.genfromtxt('data.csv', delimiter=',',
- dtype=[('id', 'i4'), ('name', 'U20'), ('value', 'f8')])
- print("\n指定数据类型加载的数据:")
- print(data_with_types)
复制代码
导出数据
- # 保存到CSV文件
- arr = np.array([[1, 2, 3], [4, 5, 6]])
- np.savetxt('output.csv', arr, delimiter=',')
- # 保存到文本文件
- np.savetxt('output.txt', arr, fmt='%d', delimiter='\t')
- # 保存到二进制文件
- np.save('output.npy', arr)
复制代码
基础数据清洗操作
数据清洗是数据预处理的重要环节,它涉及处理缺失值、重复值、异常值以及数据类型转换等问题。
缺失值处理
在实际数据集中,缺失值是常见的问题。NumPy提供了几种处理缺失值的方法:
- # 创建包含缺失值的数组
- arr = np.array([1, 2, np.nan, 4, 5, np.nan, 7])
- print("原始数组:")
- print(arr)
- # 检测缺失值
- print("\n检测缺失值:")
- print(np.isnan(arr))
- # 删除缺失值
- print("\n删除缺失值:")
- print(arr[~np.isnan(arr)])
- # 填充缺失值
- print("\n用0填充缺失值:")
- arr_filled = arr.copy()
- arr_filled[np.isnan(arr_filled)] = 0
- print(arr_filled)
- # 用均值填充缺失值
- print("\n用均值填充缺失值:")
- arr_mean_filled = arr.copy()
- mean_val = np.nanmean(arr_mean_filled) # 计算忽略NaN的均值
- arr_mean_filled[np.isnan(arr_mean_filled)] = mean_val
- print(arr_mean_filled)
复制代码
重复值处理
重复值可能会导致分析结果偏差,因此需要识别和处理:
- # 创建包含重复值的数组
- arr = np.array([1, 2, 3, 2, 4, 5, 1, 6])
- print("原始数组:")
- print(arr)
- # 查找唯一值
- print("\n唯一值:")
- print(np.unique(arr))
- # 查找重复值的索引
- print("\n重复值的索引:")
- print(np.where(np.bincount(arr) > 1)[0])
- # 删除重复值
- print("\n删除重复值:")
- unique_arr = np.unique(arr)
- print(unique_arr)
复制代码
数据类型转换
在数据清洗过程中,经常需要转换数据类型:
- # 创建不同类型的数组
- int_arr = np.array([1, 2, 3, 4])
- float_arr = np.array([1.1, 2.2, 3.3, 4.4])
- str_arr = np.array(['1', '2', '3', '4'])
- print("整数数组:", int_arr, "类型:", int_arr.dtype)
- print("浮点数组:", float_arr, "类型:", float_arr.dtype)
- print("字符串数组:", str_arr, "类型:", str_arr.dtype)
- # 类型转换
- print("\n整数转浮点:")
- print(int_arr.astype(float))
- print("\n字符串转整数:")
- print(str_arr.astype(int))
- print("\n浮点转整数:")
- print(float_arr.astype(int))
复制代码
异常值检测与处理
异常值是数据集中明显偏离其他观测值的值,可能会影响分析结果:
- # 创建包含异常值的数组
- data = np.random.normal(0, 1, 100) # 生成100个标准正态分布的随机数
- data[0] = 10 # 添加一个异常值
- data[1] = -8 # 添加另一个异常值
- print("数据统计信息:")
- print("均值:", np.mean(data))
- print("标准差:", np.std(data))
- print("最小值:", np.min(data))
- print("最大值:", np.max(data))
- # 使用Z-score检测异常值
- z_scores = (data - np.mean(data)) / np.std(data)
- threshold = 3 # 通常使用3作为阈值
- outliers = np.abs(z_scores) > threshold
- print("\nZ-score检测到的异常值索引:")
- print(np.where(outliers)[0])
- # 使用IQR方法检测异常值
- q1 = np.percentile(data, 25)
- q3 = np.percentile(data, 75)
- iqr = q3 - q1
- lower_bound = q1 - 1.5 * iqr
- upper_bound = q3 + 1.5 * iqr
- outliers_iqr = (data < lower_bound) | (data > upper_bound)
- print("\nIQR方法检测到的异常值索引:")
- print(np.where(outliers_iqr)[0])
- # 处理异常值 - 用中位数替换
- print("\n用中位数替换异常值:")
- data_cleaned = data.copy()
- median_val = np.median(data)
- data_cleaned[outliers] = median_val
- print("原始数据的前5个值:", data[:5])
- print("处理后数据的前5个值:", data_cleaned[:5])
复制代码
数据变换与规范化
数据变换和规范化是预处理的重要步骤,它们可以帮助改善数据分布,提高模型性能。
标准化
标准化是将数据转换为均值为0,标准差为1的分布:
- # 创建示例数据
- data = np.random.normal(10, 5, 100) # 均值为10,标准差为5的正态分布数据
- print("原始数据统计信息:")
- print("均值:", np.mean(data))
- print("标准差:", np.std(data))
- # Z-score标准化
- standardized_data = (data - np.mean(data)) / np.std(data)
- print("\n标准化后数据统计信息:")
- print("均值:", np.mean(standardized_data))
- print("标准差:", np.std(standardized_data))
- # 可视化对比
- import matplotlib.pyplot as plt
- plt.figure(figsize=(12, 5))
- plt.subplot(1, 2, 1)
- plt.hist(data, bins=20)
- plt.title("原始数据")
- plt.subplot(1, 2, 2)
- plt.hist(standardized_data, bins=20)
- plt.title("标准化后数据")
- plt.tight_layout()
- plt.show()
复制代码
归一化
归一化是将数据缩放到一个特定的范围,通常是[0, 1]:
- # 创建示例数据
- data = np.random.randint(0, 100, 100) # 0到100之间的随机整数
- print("原始数据统计信息:")
- print("最小值:", np.min(data))
- print("最大值:", np.max(data))
- # Min-Max归一化
- normalized_data = (data - np.min(data)) / (np.max(data) - np.min(data))
- print("\n归一化后数据统计信息:")
- print("最小值:", np.min(normalized_data))
- print("最大值:", np.max(normalized_data))
- # 可视化对比
- plt.figure(figsize=(12, 5))
- plt.subplot(1, 2, 1)
- plt.hist(data, bins=20)
- plt.title("原始数据")
- plt.subplot(1, 2, 2)
- plt.hist(normalized_data, bins=20)
- plt.title("归一化后数据")
- plt.tight_layout()
- plt.show()
复制代码
对数变换
对数变换常用于处理偏态分布,使其更接近正态分布:
- # 创建右偏数据
- data = np.random.exponential(2, 1000) # 指数分布数据
- print("原始数据统计信息:")
- print("偏度:", (np.mean((data - np.mean(data))**3)) / np.std(data)**3)
- # 对数变换
- log_transformed = np.log(data + 1) # 加1避免log(0)
- print("\n对数变换后数据统计信息:")
- print("偏度:", (np.mean((log_transformed - np.mean(log_transformed))**3)) / np.std(log_transformed)**3)
- # 可视化对比
- plt.figure(figsize=(12, 5))
- plt.subplot(1, 2, 1)
- plt.hist(data, bins=50)
- plt.title("原始数据")
- plt.subplot(1, 2, 2)
- plt.hist(log_transformed, bins=50)
- plt.title("对数变换后数据")
- plt.tight_layout()
- plt.show()
复制代码
幂变换
幂变换是另一种处理偏态分布的方法,包括Box-Cox变换等:
- from scipy import stats
- # 创建右偏数据
- data = np.random.exponential(2, 1000)
- # Box-Cox变换
- transformed_data, lambda_val = stats.boxcox(data)
- print("Box-Cox变换的lambda值:", lambda_val)
- # 可视化对比
- plt.figure(figsize=(12, 5))
- plt.subplot(1, 2, 1)
- plt.hist(data, bins=50)
- plt.title("原始数据")
- plt.subplot(1, 2, 2)
- plt.hist(transformed_data, bins=50)
- plt.title("Box-Cox变换后数据")
- plt.tight_layout()
- plt.show()
复制代码
高级数据预处理技术
除了基础的数据清洗和变换外,还有一些高级的数据预处理技术可以进一步提高数据质量。
特征缩放
特征缩放是确保不同特征具有相似尺度的重要步骤:
- # 创建不同尺度的特征
- feature1 = np.random.normal(10000, 1000, 100) # 大尺度特征
- feature2 = np.random.normal(10, 1, 100) # 小尺度特征
- features = np.column_stack((feature1, feature2))
- print("原始特征统计信息:")
- print("特征1 - 均值:", np.mean(features[:, 0]), "标准差:", np.std(features[:, 0]))
- print("特征2 - 均值:", np.mean(features[:, 1]), "标准差:", np.std(features[:, 1]))
- # 标准化
- from sklearn.preprocessing import StandardScaler
- scaler = StandardScaler()
- scaled_features = scaler.fit_transform(features)
- print("\n标准化后特征统计信息:")
- print("特征1 - 均值:", np.mean(scaled_features[:, 0]), "标准差:", np.std(scaled_features[:, 0]))
- print("特征2 - 均值:", np.mean(scaled_features[:, 1]), "标准差:", np.std(scaled_features[:, 1]))
- # 可视化对比
- plt.figure(figsize=(12, 5))
- plt.subplot(1, 2, 1)
- plt.scatter(features[:, 0], features[:, 1])
- plt.title("原始特征")
- plt.xlabel("特征1")
- plt.ylabel("特征2")
- plt.subplot(1, 2, 2)
- plt.scatter(scaled_features[:, 0], scaled_features[:, 1])
- plt.title("标准化后特征")
- plt.xlabel("特征1 (标准化)")
- plt.ylabel("特征2 (标准化)")
- plt.tight_layout()
- plt.show()
复制代码
编码分类变量
在机器学习中,我们需要将分类变量转换为数值形式:
- # 创建分类变量
- categories = np.array(['Red', 'Blue', 'Green', 'Red', 'Green', 'Blue', 'Blue'])
- # 标签编码
- from sklearn.preprocessing import LabelEncoder
- le = LabelEncoder()
- encoded_labels = le.fit_transform(categories)
- print("标签编码结果:")
- print(encoded_labels)
- print("编码映射:")
- for i, category in enumerate(le.classes_):
- print(f"{category}: {i}")
- # 独热编码
- from sklearn.preprocessing import OneHotEncoder
- ohe = OneHotEncoder(sparse=False)
- onehot_encoded = ohe.fit_transform(categories.reshape(-1, 1))
- print("\n独热编码结果:")
- print(onehot_encoded)
- print("编码映射:")
- for i, category in enumerate(ohe.categories_[0]):
- print(f"{category}: 列{i}")
复制代码
处理不平衡数据
在不平衡数据集中,某些类别的样本数量远多于其他类别:
- # 创建不平衡数据集
- class_0 = np.random.normal(0, 1, 1000) # 多数类
- class_1 = np.random.normal(3, 1, 100) # 少数类
- X = np.concatenate([class_0, class_1])
- y = np.concatenate([np.zeros(1000), np.ones(100)])
- print("原始数据集类别分布:")
- print("类别0:", np.sum(y == 0))
- print("类别1:", np.sum(y == 1))
- # 随机过采样少数类
- from imblearn.over_sampling import RandomOverSampler
- ros = RandomOverSampler(random_state=42)
- X_resampled, y_resampled = ros.fit_resample(X.reshape(-1, 1), y)
- print("\n随机过采样后类别分布:")
- print("类别0:", np.sum(y_resampled == 0))
- print("类别1:", np.sum(y_resampled == 1))
- # 随机欠采样多数类
- from imblearn.under_sampling import RandomUnderSampler
- rus = RandomUnderSampler(random_state=42)
- X_resampled, y_resampled = rus.fit_resample(X.reshape(-1, 1), y)
- print("\n随机欠采样后类别分布:")
- print("类别0:", np.sum(y_resampled == 0))
- print("类别1:", np.sum(y_resampled == 1))
- # SMOTE过采样
- from imblearn.over_sampling import SMOTE
- smote = SMOTE(random_state=42)
- X_resampled, y_resampled = smote.fit_resample(X.reshape(-1, 1), y)
- print("\nSMOTE过采样后类别分布:")
- print("类别0:", np.sum(y_resampled == 0))
- print("类别1:", np.sum(y_resampled == 1))
复制代码
数据降维
数据降维可以减少特征数量,同时保留数据集的重要信息:
- # 创建高维数据
- np.random.seed(42)
- mean = np.zeros(10)
- cov = np.eye(10)
- data = np.random.multivariate_normal(mean, cov, 100)
- print("原始数据形状:", data.shape)
- # PCA降维
- from sklearn.decomposition import PCA
- pca = PCA(n_components=2)
- pca_result = pca.fit_transform(data)
- print("\nPCA降维后数据形状:", pca_result.shape)
- print("解释方差比:", pca.explained_variance_ratio_)
- # 可视化
- plt.figure(figsize=(8, 6))
- plt.scatter(pca_result[:, 0], pca_result[:, 1])
- plt.title("PCA降维结果")
- plt.xlabel("主成分1")
- plt.ylabel("主成分2")
- plt.show()
- # t-SNE降维
- from sklearn.manifold import TSNE
- tsne = TSNE(n_components=2, random_state=42)
- tsne_result = tsne.fit_transform(data)
- print("\nt-SNE降维后数据形状:", tsne_result.shape)
- # 可视化
- plt.figure(figsize=(8, 6))
- plt.scatter(tsne_result[:, 0], tsne_result[:, 1])
- plt.title("t-SNE降维结果")
- plt.xlabel("维度1")
- plt.ylabel("维度2")
- plt.show()
复制代码
数据整合与重塑
在数据预处理过程中,经常需要整合和重塑数据以适应不同的分析需求。
数组拼接与分割
- # 创建示例数组
- arr1 = np.array([[1, 2, 3], [4, 5, 6]])
- arr2 = np.array([[7, 8, 9], [10, 11, 12]])
- # 垂直拼接
- vstack_result = np.vstack((arr1, arr2))
- print("垂直拼接结果:")
- print(vstack_result)
- # 水平拼接
- hstack_result = np.hstack((arr1, arr2))
- print("\n水平拼接结果:")
- print(hstack_result)
- # 深度拼接
- dstack_result = np.dstack((arr1, arr2))
- print("\n深度拼接结果:")
- print(dstack_result)
- print("深度拼接结果形状:", dstack_result.shape)
- # 数组分割
- arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16]])
- # 垂直分割
- vsplit_result = np.vsplit(arr, 2)
- print("\n垂直分割结果:")
- for i, sub_arr in enumerate(vsplit_result):
- print(f"子数组 {i+1}:")
- print(sub_arr)
- # 水平分割
- hsplit_result = np.hsplit(arr, 2)
- print("\n水平分割结果:")
- for i, sub_arr in enumerate(hsplit_result):
- print(f"子数组 {i+1}:")
- print(sub_arr)
复制代码
形状变换
- # 创建一维数组
- arr = np.arange(1, 13)
- print("原始一维数组:")
- print(arr)
- # 转换为二维数组
- reshaped_2d = arr.reshape(3, 4)
- print("\n转换为3x4二维数组:")
- print(reshaped_2d)
- # 转换为三维数组
- reshaped_3d = arr.reshape(2, 3, 2)
- print("\n转换为2x3x2三维数组:")
- print(reshaped_3d)
- # 使用-1自动计算维度
- auto_reshaped = arr.reshape(3, -1)
- print("\n使用-1自动计算列数:")
- print(auto_reshaped)
- # 展平数组
- flattened = reshaped_3d.flatten()
- print("\n展平三维数组:")
- print(flattened)
- # 转置
- transposed = reshaped_2d.T
- print("\n转置二维数组:")
- print(transposed)
复制代码
转置与轴变换
- # 创建三维数组
- arr = np.arange(24).reshape(2, 3, 4)
- print("原始三维数组:")
- print(arr)
- print("原始形状:", arr.shape)
- # 转置
- transposed = arr.T
- print("\n转置后数组:")
- print(transposed)
- print("转置后形状:", transposed.shape)
- # 交换轴
- swapped = np.swapaxes(arr, 0, 2) # 交换第0轴和第2轴
- print("\n交换第0轴和第2轴后:")
- print(swapped)
- print("交换轴后形状:", swapped.shape)
- # 使用transpose函数
- transposed_func = np.transpose(arr, (2, 0, 1)) # 将轴的顺序从(0,1,2)变为(2,0,1)
- print("\n使用transpose函数改变轴顺序:")
- print(transposed_func)
- print("改变轴顺序后形状:", transposed_func.shape)
复制代码
性能优化技巧
在处理大型数据集时,性能优化至关重要。NumPy提供了多种优化技术,可以显著提高数据处理效率。
向量化操作
向量化是NumPy的核心优势之一,它可以避免使用显式循环,从而大幅提高性能:
- # 创建大型数组
- size = 1000000
- a = np.random.rand(size)
- b = np.random.rand(size)
- # 使用循环计算
- import time
- start_time = time.time()
- result_loop = []
- for i in range(size):
- result_loop.append(a[i] + b[i])
- loop_time = time.time() - start_time
- print(f"循环计算时间: {loop_time:.4f}秒")
- # 使用向量化操作
- start_time = time.time()
- result_vectorized = a + b
- vectorized_time = time.time() - start_time
- print(f"向量化计算时间: {vectorized_time:.4f}秒")
- # 性能提升
- speedup = loop_time / vectorized_time
- print(f"向量化操作比循环快 {speedup:.2f} 倍")
复制代码
广播机制
广播是NumPy的另一个强大功能,它允许不同形状的数组进行算术运算:
- # 创建不同形状的数组
- a = np.array([[1, 2, 3], [4, 5, 6]]) # 形状(2, 3)
- b = np.array([10, 20, 30]) # 形状(3,)
- # 广播加法
- result = a + b
- print("数组a:")
- print(a)
- print("\n数组b:")
- print(b)
- print("\n广播加法结果:")
- print(result)
- # 创建另一个广播示例
- c = np.array([[10], [20]]) # 形状(2, 1)
- result2 = a + c
- print("\n数组c:")
- print(c)
- print("\n广播加法结果2:")
- print(result2)
- # 广播规则说明
- print("\n广播规则:")
- print("1. 如果数组的维度不同,在形状较小的数组前面补1")
- print("2. 如果两个数组在某个维度上的大小相同,或者其中一个为1,则它们可以广播")
- print("3. 如果两个数组在所有维度上都兼容,则可以广播")
- print("4. 广播后,每个数组的行为就像它的形状是两个输入数组的形状的逐元素最大值")
- print("5. 在任何一个维度为1且另一个维度大于1的维度上,前者就像沿着该维度复制了后者一样")
复制代码
内存优化
处理大型数据集时,内存管理非常重要:
- # 检查数组内存使用
- arr = np.zeros((1000, 1000))
- print("数组内存使用 (MB):", arr.nbytes / (1024 * 1024))
- # 使用适当的数据类型减少内存使用
- arr_float64 = np.zeros((1000, 1000), dtype=np.float64)
- print("float64数组内存使用 (MB):", arr_float64.nbytes / (1024 * 1024))
- arr_float32 = np.zeros((1000, 1000), dtype=np.float32)
- print("float32数组内存使用 (MB):", arr_float32.nbytes / (1024 * 1024))
- arr_int32 = np.zeros((1000, 1000), dtype=np.int32)
- print("int32数组内存使用 (MB):", arr_int32.nbytes / (1024 * 1024))
- # 使用内存映射文件处理大型数组
- # 创建一个大型数组并保存到磁盘
- large_array = np.random.rand(10000, 10000)
- np.save('large_array.npy', large_array)
- # 使用内存映射加载数组
- memmap_array = np.load('large_array.npy', mmap_mode='r')
- print("\n内存映射数组类型:", type(memmap_array))
- print("内存映射数组形状:", memmap_array.shape)
- # 使用生成器处理大型数据集
- def array_generator(shape, chunk_size=1000):
- """生成大型数组的块"""
- rows, cols = shape
- for i in range(0, rows, chunk_size):
- yield np.random.rand(min(chunk_size, rows - i), cols)
- # 处理大型数组而不将其全部加载到内存
- total_sum = 0
- for chunk in array_generator((10000, 10000)):
- total_sum += np.sum(chunk)
- print("\n生成器处理大型数组的总和:", total_sum)
复制代码
实际案例分析
让我们通过一个实际案例来应用我们学到的NumPy数据清洗和预处理技术。假设我们有一个包含客户信息的数据集,我们需要对其进行清洗和预处理,以便进行后续分析。
- # 创建模拟数据集
- np.random.seed(42)
- num_customers = 1000
- # 客户ID
- customer_ids = np.arange(1, num_customers + 1)
- # 年龄 (18-80岁,有一些异常值)
- ages = np.random.normal(40, 15, num_customers)
- ages = np.clip(ages, 18, 80) # 限制在18-80岁之间
- ages[:10] = np.random.choice([150, -10, 999], 10) # 添加一些异常值
- # 收入 (20000-200000,有一些缺失值)
- incomes = np.random.normal(80000, 30000, num_customers)
- incomes = np.clip(incomes, 20000, 200000)
- incomes[np.random.choice(num_customers, 50, replace=False)] = np.nan # 添加缺失值
- # 消费金额 (0-10000,有一些异常值)
- spending = np.random.exponential(1000, num_customers)
- spending = np.clip(spending, 0, 10000)
- spending[:5] = np.random.choice([50000, -1000, 99999], 5) # 添加一些异常值
- # 性别 (M/F,有一些缺失值)
- genders = np.random.choice(['M', 'F'], num_customers)
- genders[np.random.choice(num_customers, 30, replace=False)] = np.nan # 添加缺失值
- # 创建数据集
- data = np.column_stack((customer_ids, ages, incomes, spending, genders))
- print("原始数据集前10行:")
- print(data[:10])
- # 数据清洗和预处理步骤
- # 1. 处理异常值
- # 年龄异常值处理
- age_mask = (data[:, 1] < 18) | (data[:, 1] > 80)
- data[age_mask, 1] = np.nan # 将异常年龄设为缺失值
- # 收入异常值处理
- income_mask = (data[:, 2] < 20000) | (data[:, 2] > 200000)
- data[income_mask, 2] = np.nan # 将异常收入设为缺失值
- # 消费金额异常值处理
- spending_mask = (data[:, 3] < 0) | (data[:, 3] > 10000)
- data[spending_mask, 3] = np.nan # 将异常消费设为缺失值
- print("\n处理异常值后数据集前10行:")
- print(data[:10])
- # 2. 处理缺失值
- # 数值列用中位数填充
- numeric_cols = [1, 2, 3] # 年龄、收入、消费列
- for col in numeric_cols:
- col_data = data[:, col].astype(float)
- median_val = np.nanmedian(col_data)
- data[np.isnan(col_data), col] = median_val
- # 分类列用众数填充
- from scipy import stats
- gender_col = data[:, 4]
- mode_val = stats.mode(gender_col[~np.isnan(gender_col)])[0][0]
- data[np.isnan(gender_col), 4] = mode_val
- print("\n处理缺失值后数据集前10行:")
- print(data[:10])
- # 3. 数据转换
- # 将性别转换为数值
- gender_map = {'M': 0, 'F': 1}
- data[:, 4] = np.array([gender_map[g] for g in data[:, 4]])
- # 4. 特征缩放
- # 标准化数值特征
- from sklearn.preprocessing import StandardScaler
- scaler = StandardScaler()
- data[:, 1:4] = scaler.fit_transform(data[:, 1:4].astype(float))
- print("\n标准化后数据集前10行:")
- print(data[:10])
- # 5. 创建新特征
- # 收入与消费比例
- income_spending_ratio = data[:, 2].astype(float) / (data[:, 3].astype(float) + 1e-6) # 避免除以零
- data = np.column_stack((data, income_spending_ratio))
- # 年龄分组
- age_groups = np.digitize(data[:, 1].astype(float), bins=[18, 25, 35, 45, 55, 65])
- data = np.column_stack((data, age_groups))
- print("\n添加新特征后数据集前10行:")
- print(data[:10])
- # 6. 数据分析
- # 计算基本统计信息
- print("\n清洗后数据集统计信息:")
- print("平均年龄:", np.mean(data[:, 1]))
- print("平均收入:", np.mean(data[:, 2]))
- print("平均消费:", np.mean(data[:, 3]))
- print("男性比例:", np.mean(data[:, 4] == 0))
- print("女性比例:", np.mean(data[:, 4] == 1))
- # 相关性分析
- numeric_data = data[:, [1, 2, 3, 5]].astype(float) # 选择数值列
- corr_matrix = np.corrcoef(numeric_data.T)
- print("\n相关性矩阵:")
- print(corr_matrix)
- # 可视化
- import matplotlib.pyplot as plt
- plt.figure(figsize=(15, 10))
- # 年龄分布
- plt.subplot(2, 2, 1)
- plt.hist(data[:, 1], bins=20)
- plt.title("年龄分布")
- plt.xlabel("年龄 (标准化)")
- plt.ylabel("频数")
- # 收入分布
- plt.subplot(2, 2, 2)
- plt.hist(data[:, 2], bins=20)
- plt.title("收入分布")
- plt.xlabel("收入 (标准化)")
- plt.ylabel("频数")
- # 消费分布
- plt.subplot(2, 2, 3)
- plt.hist(data[:, 3], bins=20)
- plt.title("消费分布")
- plt.xlabel("消费 (标准化)")
- plt.ylabel("频数")
- # 收入与消费关系
- plt.subplot(2, 2, 4)
- plt.scatter(data[:, 2], data[:, 3], alpha=0.5)
- plt.title("收入与消费关系")
- plt.xlabel("收入 (标准化)")
- plt.ylabel("消费 (标准化)")
- plt.tight_layout()
- plt.show()
复制代码
这个实际案例展示了如何使用NumPy进行完整的数据清洗和预处理流程,包括处理异常值、缺失值、数据转换、特征缩放、特征创建和基本数据分析。通过这些步骤,我们将原始的、有问题的数据集转换为了干净、标准化的数据,可以用于后续的分析和建模工作。
最佳实践与常见陷阱
在使用NumPy进行数据清洗和预处理时,有一些最佳实践和常见陷阱需要注意:
最佳实践
1. 数据备份:在进行任何数据转换之前,始终创建原始数据的备份:
- # 创建原始数据的备份
- original_data = data.copy()
复制代码
1. 逐步处理:将数据清洗和预处理过程分解为多个小步骤,每步后验证结果:
- # 逐步处理数据
- # 步骤1: 处理异常值
- data = handle_outliers(data)
- print("步骤1完成: 处理异常值")
- # 步骤2: 处理缺失值
- data = handle_missing_values(data)
- print("步骤2完成: 处理缺失值")
- # 步骤3: 数据转换
- data = transform_data(data)
- print("步骤3完成: 数据转换")
复制代码
1. 使用适当的数据类型:选择合适的数据类型可以节省内存并提高性能:
- # 使用适当的数据类型
- int_array = np.array([1, 2, 3, 4], dtype=np.int8) # 小整数
- float_array = np.array([1.0, 2.0, 3.0], dtype=np.float32) # 单精度浮点数
- bool_array = np.array([True, False, True], dtype=np.bool) # 布尔值
复制代码
1. 文档记录:记录所有数据清洗和预处理步骤,以便重现结果:
- # 创建处理日志
- processing_log = [
- "步骤1: 删除了重复行",
- "步骤2: 用中位数填充了缺失值",
- "步骤3: 标准化了数值特征",
- "步骤4: 对分类变量进行了独热编码"
- ]
- # 保存日志
- with open('processing_log.txt', 'w') as f:
- for step in processing_log:
- f.write(step + '\n')
复制代码
1. 验证结果:在每个步骤后验证数据,确保没有引入错误:
- # 验证数据
- def validate_data(data):
- """验证数据的基本完整性"""
- # 检查是否有NaN值
- if np.isnan(data).any():
- print("警告: 数据中存在NaN值")
-
- # 检查是否有无限值
- if np.isinf(data).any():
- print("警告: 数据中存在无限值")
-
- # 检查数据形状
- print(f"数据形状: {data.shape}")
-
- # 检查数据类型
- print(f"数据类型: {data.dtype}")
-
- return True
- # 使用验证函数
- validate_data(data)
复制代码
常见陷阱
1. 修改视图而非副本:NumPy中的切片操作返回的是视图而非副本,修改视图会影响原始数组:
- # 常见陷阱: 修改视图影响原始数组
- arr = np.array([1, 2, 3, 4, 5])
- subset = arr[:3] # 这是视图,不是副本
- subset[0] = 100 # 这会修改原始数组
- print("原始数组:", arr) # 输出: [100 2 3 4 5]
- # 正确做法: 创建副本
- arr = np.array([1, 2, 3, 4, 5])
- subset = arr[:3].copy() # 创建副本
- subset[0] = 100 # 这不会修改原始数组
- print("原始数组:", arr) # 输出: [1 2 3 4 5]
复制代码
1. 忽略广播规则:不正确地使用广播可能导致意外的结果:
- # 常见陷阱: 广播形状不匹配
- a = np.array([[1, 2, 3], [4, 5, 6]]) # 形状(2, 3)
- b = np.array([1, 2]) # 形状(2,)
- try:
- result = a + b # 这会引发错误
- except ValueError as e:
- print("错误:", e)
- # 正确做法: 确保广播兼容
- a = np.array([[1, 2, 3], [4, 5, 6]]) # 形状(2, 3)
- b = np.array([[1], [2]]) # 形状(2, 1),可以广播到(2, 3)
- result = a + b
- print("广播结果:")
- print(result)
复制代码
1. 内存使用不当:处理大型数组时,不注意内存使用可能导致性能问题:
- # 常见陷阱: 不必要的内存使用
- # 创建多个大型数组
- large_arrays = [np.random.rand(10000, 10000) for _ in range(5)]
- # 这会消耗大量内存
- # 正确做法: 一次处理一个数组或使用内存映射
- result = np.zeros((10000, 10000))
- for i in range(5):
- # 一次处理一个数组
- temp = np.random.rand(10000, 10000)
- result += temp
- del temp # 释放内存
- result /= 5
复制代码
1. 链式索引:使用链式索引可能导致意外行为:
- # 常见陷阱: 链式索引
- arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
- try:
- # 这会引发警告,因为arr[0, 1]返回的是副本,不是视图
- arr[0][1] = 100
- except Exception as e:
- print("错误:", e)
- # 正确做法: 使用逗号分隔的索引
- arr = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
- arr[0, 1] = 100 # 这是正确的方式
- print("修改后的数组:")
- print(arr)
复制代码
1. 忽略数据类型:不注意数据类型可能导致精度损失或意外结果:
- # 常见陷阱: 数据类型转换导致精度损失
- float_arr = np.array([1.1, 2.2, 3.3])
- int_arr = float_arr.astype(int) # 这会丢失小数部分
- print("原始浮点数组:", float_arr)
- print("转换为整数后的数组:", int_arr)
- # 正确做法: 使用适当的数据类型
- float_arr = np.array([1.1, 2.2, 3.3], dtype=np.float64) # 明确指定高精度
- rounded_arr = np.round(float_arr).astype(int) # 先四舍五入再转换
- print("四舍五入后的整数数组:", rounded_arr)
复制代码
通过遵循这些最佳实践并避免常见陷阱,你可以更有效地使用NumPy进行数据清洗和预处理,提高数据分析的质量和准确性。
结论与展望
本指南全面介绍了使用NumPy进行数据清洗和预处理的各个方面,从基础操作到高级技巧。我们学习了如何处理缺失值、异常值和重复值,如何进行数据变换和规范化,以及如何应用高级预处理技术如特征缩放、编码分类变量、处理不平衡数据和数据降维。我们还探讨了数据整合与重塑、性能优化技巧,并通过实际案例展示了这些技术的应用。
NumPy作为Python数据科学生态系统的核心库,提供了强大而高效的工具来处理各种数据清洗和预处理任务。通过掌握这些技术,你可以显著提高数据分析的质量和准确性,为后续的建模和分析工作奠定坚实的基础。
随着数据科学领域的不断发展,NumPy也在不断演进。未来,我们可以期待看到更多针对大规模数据处理、分布式计算和与新兴机器学习框架集成的功能。同时,随着硬件技术的发展,NumPy可能会进一步优化其性能,更好地利用GPU和TPU等加速器。
作为数据科学家或分析师,持续学习和掌握NumPy的新功能和最佳实践是非常重要的。通过本指南提供的基础知识和技巧,你已经具备了使用NumPy进行高效数据清洗和预处理的能力。希望这些知识能够帮助你在实际工作中更好地处理各类数据集,提升数据分析的质量与准确性。 |
|