目标:掌握数据导入、缺失值/重复值处理、类型转换、异常值检测、文本清洗、标准化与归一化、列名调整、分类变量编码,以及一个综合案例(清洗问卷数据)。
工具:pandas、numpy、(可选)scikit-learn 的预处理模块。
本章延续“讲解 → 示范 → 举例 → 注意事项”的结构。所有代码均含中文注释,便于课堂演示与自学。
讲解:数据分析前先导入数据并快速“体检”:查看行列规模、前几行、数据类型与基本统计。
常用函数:pd.read_csv()、df.head()、df.info()、df.describe()。
import pandas as pd
import numpy as np
# 构造一个模拟的问卷数据 DataFrame(也可换成 pd.read_csv('yourfile.csv') 导入实际数据)
data = {
"id": [1, 2, 3, 4, 5, 5], # 故意放一个重复 id=5
"name": ["张三", "李四", "王五", "赵六", "周七", "周七"],
"gender": ["男", "女", "女", None, "男", "男"], # None 表示缺失
"age": [20, 22, None, 21, 200, 21], # 200 明显异常,第三个缺失
"major": ["社会学", "历史学", "文学", "社会学", "历史学", "历史学"],
"join_date": ["2025-03-01", "2025/03/02", "2025-03-05", "2025-03-08", "03-10-2025", "2025-03-10"],
"q1_satisfaction": [5, 4, 3, None, 5, 5],
"q2_hours_ai": ["2", "3", "1", "2", "100", "2"], # 100 可能是输入错误
"free_text": ["课程很好!", "讲解清晰 ", "内容太难;希望更多例子", " 老师讲得不错", "很好", "很好"]
}
df = pd.DataFrame(data)
# 初步查看
print("行列规模:", df.shape)
display(df.head(3)) # 查看前3行
print("\n数据类型信息:")
print(df.info())
print("\n基本统计:")
display(df.describe(include='all'))
C:\Users\Zhouq\AppData\Roaming\Python\Python39\site-packages\pandas\core\computation\expressions.py:21: UserWarning: Pandas requires version '2.8.4' or newer of 'numexpr' (version '2.8.3' currently installed). from pandas.core.computation.check import NUMEXPR_INSTALLED C:\Users\Zhouq\AppData\Roaming\Python\Python39\site-packages\pandas\core\arrays\masked.py:60: UserWarning: Pandas requires version '1.3.6' or newer of 'bottleneck' (version '1.3.5' currently installed). from pandas.core import (
行列规模: (6, 9)
| id | name | gender | age | major | join_date | q1_satisfaction | q2_hours_ai | free_text | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 张三 | 男 | 20.0 | 社会学 | 2025-03-01 | 5.0 | 2 | 课程很好! |
| 1 | 2 | 李四 | 女 | 22.0 | 历史学 | 2025/03/02 | 4.0 | 3 | 讲解清晰 |
| 2 | 3 | 王五 | 女 | NaN | 文学 | 2025-03-05 | 3.0 | 1 | 内容太难;希望更多例子 |
数据类型信息: <class 'pandas.core.frame.DataFrame'> RangeIndex: 6 entries, 0 to 5 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 6 non-null int64 1 name 6 non-null object 2 gender 5 non-null object 3 age 5 non-null float64 4 major 6 non-null object 5 join_date 6 non-null object 6 q1_satisfaction 5 non-null float64 7 q2_hours_ai 6 non-null object 8 free_text 6 non-null object dtypes: float64(2), int64(1), object(6) memory usage: 560.0+ bytes None 基本统计:
| id | name | gender | age | major | join_date | q1_satisfaction | q2_hours_ai | free_text | |
|---|---|---|---|---|---|---|---|---|---|
| count | 6.000000 | 6 | 5 | 5.000000 | 6 | 6 | 5.000000 | 6 | 6 |
| unique | NaN | 5 | 2 | NaN | 3 | 6 | NaN | 4 | 5 |
| top | NaN | 周七 | 男 | NaN | 历史学 | 2025-03-01 | NaN | 2 | 很好 |
| freq | NaN | 2 | 3 | NaN | 3 | 1 | NaN | 3 | 2 |
| mean | 3.333333 | NaN | NaN | 56.800000 | NaN | NaN | 4.400000 | NaN | NaN |
| std | 1.632993 | NaN | NaN | 80.054357 | NaN | NaN | 0.894427 | NaN | NaN |
| min | 1.000000 | NaN | NaN | 20.000000 | NaN | NaN | 3.000000 | NaN | NaN |
| 25% | 2.250000 | NaN | NaN | 21.000000 | NaN | NaN | 4.000000 | NaN | NaN |
| 50% | 3.500000 | NaN | NaN | 21.000000 | NaN | NaN | 5.000000 | NaN | NaN |
| 75% | 4.750000 | NaN | NaN | 22.000000 | NaN | NaN | 5.000000 | NaN | NaN |
| max | 5.000000 | NaN | NaN | 200.000000 | NaN | NaN | 5.000000 | NaN | NaN |
讲解:缺失值会影响统计与建模。常见策略:
df.isna().sum() dropna()(谨慎使用,可能丢失信息) fillna()(均值/中位数/众数/固定值)# 检测缺失值
print("各列缺失值数:\n", df.isna().sum())
# 示例1:用众数填充 gender;用中位数填充 age;用指定值填充 q1_satisfaction
gender_mode = df['gender'].mode(dropna=True)[0]
df['gender'] = df['gender'].fillna(gender_mode)
age_median = df['age'].median(skipna=True)
df['age'] = df['age'].fillna(age_median)
df['q1_satisfaction'] = df['q1_satisfaction'].fillna(3) # 以3作为中性填充示例
print("\n缺失处理后各列缺失值数:\n", df.isna().sum())
display(df.head())
各列缺失值数: id 0 name 0 gender 1 age 1 major 0 join_date 0 q1_satisfaction 1 q2_hours_ai 0 free_text 0 dtype: int64 缺失处理后各列缺失值数: id 0 name 0 gender 0 age 0 major 0 join_date 0 q1_satisfaction 0 q2_hours_ai 0 free_text 0 dtype: int64
| id | name | gender | age | major | join_date | q1_satisfaction | q2_hours_ai | free_text | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 张三 | 男 | 20.0 | 社会学 | 2025-03-01 | 5.0 | 2 | 课程很好! |
| 1 | 2 | 李四 | 女 | 22.0 | 历史学 | 2025/03/02 | 4.0 | 3 | 讲解清晰 |
| 2 | 3 | 王五 | 女 | 21.0 | 文学 | 2025-03-05 | 3.0 | 1 | 内容太难;希望更多例子 |
| 3 | 4 | 赵六 | 男 | 21.0 | 社会学 | 2025-03-08 | 3.0 | 2 | 老师讲得不错 |
| 4 | 5 | 周七 | 男 | 200.0 | 历史学 | 03-10-2025 | 5.0 | 100 | 很好 |
讲解:重复记录会导致统计失真。
df.duplicated() df.drop_duplicates()id 多条记录时,可按时间或数据完整度选择保留其中一条。# 检测重复行
print("是否存在重复行:", df.duplicated().any())
print("重复行索引:", df[df.duplicated()].index.tolist())
# 简单策略:直接去重(保留第一次出现)
df = df.drop_duplicates().reset_index(drop=True)
print("去重后形状:", df.shape)
display(df)
是否存在重复行: False 重复行索引: [] 去重后形状: (6, 9)
| id | name | gender | age | major | join_date | q1_satisfaction | q2_hours_ai | free_text | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 张三 | 男 | 20.0 | 社会学 | 2025-03-01 | 5.0 | 2 | 课程很好! |
| 1 | 2 | 李四 | 女 | 22.0 | 历史学 | 2025/03/02 | 4.0 | 3 | 讲解清晰 |
| 2 | 3 | 王五 | 女 | 21.0 | 文学 | 2025-03-05 | 3.0 | 1 | 内容太难;希望更多例子 |
| 3 | 4 | 赵六 | 男 | 21.0 | 社会学 | 2025-03-08 | 3.0 | 2 | 老师讲得不错 |
| 4 | 5 | 周七 | 男 | 200.0 | 历史学 | 03-10-2025 | 5.0 | 100 | 很好 |
| 5 | 5 | 周七 | 男 | 21.0 | 历史学 | 2025-03-10 | 5.0 | 2 | 很好 |
讲解:原始数据常以字符串形式出现,需要转换:
pd.to_numeric(errors='coerce') pd.to_datetime(errors='coerce')(可自动识别常见格式) astype('category')NaT/NaN,后续再处理。# 将 q2_hours_ai 转为数值(遇到无法转换的设为 NaN)
df['q2_hours_ai'] = pd.to_numeric(df['q2_hours_ai'], errors='coerce')
# 将 join_date 转为日期
df['join_date'] = pd.to_datetime(df['join_date'], errors='coerce')
# 将 major 转为类别类型(节省内存、利于分类分析)
df['major'] = df['major'].astype('category')
print(df.dtypes)
display(df[['q2_hours_ai','join_date','major']].head())
id int64 name object gender object age float64 major category join_date datetime64[ns] q1_satisfaction float64 q2_hours_ai int64 free_text object dtype: object
| q2_hours_ai | join_date | major | |
|---|---|---|---|
| 0 | 2 | 2025-03-01 | 社会学 |
| 1 | 3 | NaT | 历史学 |
| 2 | 1 | 2025-03-05 | 文学 |
| 3 | 2 | 2025-03-08 | 社会学 |
| 4 | 100 | NaT | 历史学 |
讲解:异常值可能来自录入错误或极端情况。
0<age<100 Q1-1.5*IQR 至 Q3+1.5*IQR 之外为异常# 简单规则:年龄在[0,100]之外视为异常,设为 NaN 再用中位数填充
outlier_mask_age = (df['age'] < 0) | (df['age'] > 100)
print("异常年龄数量:", outlier_mask_age.sum())
df.loc[outlier_mask_age, 'age'] = np.nan
df['age'] = df['age'].fillna(df['age'].median())
# 对学习AI小时数进行 IQR 检测
q1 = df['q2_hours_ai'].quantile(0.25)
q3 = df['q2_hours_ai'].quantile(0.75)
iqr = q3 - q1
lower, upper = q1 - 1.5*iqr, q3 + 1.5*iqr
print("q2_hours_ai 合理范围:", (lower, upper))
# 将超过范围的值设为上/下阈值(winsorize 裁剪示例)
df['q2_hours_ai'] = df['q2_hours_ai'].clip(lower, upper)
display(df[['age','q2_hours_ai']])
异常年龄数量: 1 q2_hours_ai 合理范围: (0.875, 3.875)
| age | q2_hours_ai | |
|---|---|---|
| 0 | 20.0 | 2.000 |
| 1 | 22.0 | 3.000 |
| 2 | 21.0 | 1.000 |
| 3 | 21.0 | 2.000 |
| 4 | 21.0 | 3.875 |
| 5 | 21.0 | 2.000 |
讲解:文本常见清洗动作:
str.strip() str.replace(r"\s+", " ", regex=True) str.lower() # 对 free_text 进行简单清洗
df['free_text_clean'] = (
df['free_text']
.astype(str)
.str.strip()
.str.replace(r"\s+", " ", regex=True) # 统一多空白为单空格
.str.replace(";", ";") # 统一中文分号为英文分号
.str.replace(",", ",") # 统一中文逗号为英文逗号
)
display(df[['free_text','free_text_clean']])
| free_text | free_text_clean | |
|---|---|---|
| 0 | 课程很好! | 课程很好! |
| 1 | 讲解清晰 | 讲解清晰 |
| 2 | 内容太难;希望更多例子 | 内容太难;希望更多例子 |
| 3 | 老师讲得不错 | 老师讲得不错 |
| 4 | 很好 | 很好 |
| 5 | 很好 | 很好 |
讲解:为消除量纲影响或提升模型稳定性,常对数值变量做:
(x-mean)/std) [0,1]((x-min)/(max-min))from sklearn.preprocessing import StandardScaler, MinMaxScaler
num_cols = ['age', 'q1_satisfaction', 'q2_hours_ai']
num_df = df[num_cols].copy()
# 标准化
std_scaler = StandardScaler()
std_scaled = std_scaler.fit_transform(num_df)
std_scaled_df = pd.DataFrame(std_scaled, columns=[c+"_std" for c in num_cols])
# 归一化
mm_scaler = MinMaxScaler()
mm_scaled = mm_scaler.fit_transform(num_df)
mm_scaled_df = pd.DataFrame(mm_scaled, columns=[c+"_minmax" for c in num_cols])
scaled_result = pd.concat([df[num_cols], std_scaled_df, mm_scaled_df], axis=1)
display(scaled_result.head())
| age | q1_satisfaction | q2_hours_ai | age_std | q1_satisfaction_std | q2_hours_ai_std | age_minmax | q1_satisfaction_minmax | q2_hours_ai_minmax | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 20.0 | 5.0 | 2.000 | -1.732051 | 0.928477 | -0.344759 | 0.0 | 1.0 | 0.347826 |
| 1 | 22.0 | 4.0 | 3.000 | 1.732051 | -0.185695 | 0.758470 | 1.0 | 0.5 | 0.695652 |
| 2 | 21.0 | 3.0 | 1.000 | 0.000000 | -1.299867 | -1.447989 | 0.5 | 0.0 | 0.000000 |
| 3 | 21.0 | 3.0 | 2.000 | 0.000000 | -1.299867 | -0.344759 | 0.5 | 0.0 | 0.347826 |
| 4 | 21.0 | 5.0 | 3.875 | 0.000000 | 0.928477 | 1.723796 | 0.5 | 1.0 | 1.000000 |
# 统一英文化列名(示例)
rename_map = {
"name": "Name", "gender": "Gender", "major": "Major",
"join_date": "JoinDate", "q1_satisfaction": "Q1_Satisfaction",
"q2_hours_ai": "Q2_HoursAI", "free_text": "FreeText", "free_text_clean": "FreeTextClean"
}
df2 = df.rename(columns=rename_map)
# 重新排序
new_order = ["id", "Name", "Gender", "age", "Major", "JoinDate",
"Q1_Satisfaction", "Q2_HoursAI", "FreeText", "FreeTextClean"]
df2 = df2[new_order]
display(df2.head())
| id | Name | Gender | age | Major | JoinDate | Q1_Satisfaction | Q2_HoursAI | FreeText | FreeTextClean | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 张三 | 男 | 20.0 | 社会学 | 2025-03-01 | 5.0 | 2.000 | 课程很好! | 课程很好! |
| 1 | 2 | 李四 | 女 | 22.0 | 历史学 | NaT | 4.0 | 3.000 | 讲解清晰 | 讲解清晰 |
| 2 | 3 | 王五 | 女 | 21.0 | 文学 | 2025-03-05 | 3.0 | 1.000 | 内容太难;希望更多例子 | 内容太难;希望更多例子 |
| 3 | 4 | 赵六 | 男 | 21.0 | 社会学 | 2025-03-08 | 3.0 | 2.000 | 老师讲得不错 | 老师讲得不错 |
| 4 | 5 | 周七 | 男 | 21.0 | 历史学 | NaT | 5.0 | 3.875 | 很好 | 很好 |
讲解:模型常需要数值特征:
from sklearn.preprocessing import LabelEncoder
# Label Encoding(对 Gender)
le = LabelEncoder()
df2['Gender_le'] = le.fit_transform(df2['Gender'].astype(str))
# One-Hot(对 Major)
df_ohe = pd.get_dummies(df2, columns=['Major'], prefix='Major')
display(df2[['Gender','Gender_le']].head())
display(df_ohe.filter(like='Major_').head())
| Gender | Gender_le | |
|---|---|---|
| 0 | 男 | 1 |
| 1 | 女 | 0 |
| 2 | 女 | 0 |
| 3 | 男 | 1 |
| 4 | 男 | 1 |
| Major_历史学 | Major_文学 | Major_社会学 | |
|---|---|---|---|
| 0 | False | False | True |
| 1 | True | False | False |
| 2 | False | True | False |
| 3 | False | False | True |
| 4 | True | False | False |
任务:对第 10.1 的“原始”问卷数据完成以下步骤并得到“可分析版”:
# 1) 复制原始数据(此处用 df2 的英文化版本继续)
clean = df2.copy()
log = [] # 简单字符串日志列表
# 2) 去重
dup_n = clean.duplicated().sum()
if dup_n > 0:
log.append(f"去重:删除 {dup_n} 行重复记录")
clean = clean.drop_duplicates().reset_index(drop=True)
# 3) 类型检查:已经在前面做过,这里示意追加说明
log.append("类型转换:Q2_HoursAI->数值;JoinDate->日期;Major->分类")
# 4) 缺失与异常:年龄限制到[0,100],缺失用中位数填充
before_na = clean.isna().sum().sum()
clean['age'] = clean['age'].mask((clean['age']<0)|(clean['age']>100), np.nan)
clean['age'] = clean['age'].fillna(clean['age'].median())
after_na = clean.isna().sum().sum()
log.append(f"缺失/异常:总缺失从 {before_na} 降到 {after_na}")
# 5) 文本清洗:已在 FreeTextClean 中
log.append("文本清洗:生成 FreeTextClean(strip/多空白合一/标点统一)")
# 6) 编码:性别 Label Encoding;专业 One-Hot
from sklearn.preprocessing import LabelEncoder
le2 = LabelEncoder()
clean['Gender_le'] = le2.fit_transform(clean['Gender'].astype(str))
clean = pd.get_dummies(clean, columns=['Major'], prefix='Major')
log.append("编码:Gender->LabelEncoding;Major->One-Hot")
# 7) 导出可分析子集(示例)
analysis_cols = ['id','Name','Gender','Gender_le','age','JoinDate','Q1_Satisfaction','Q2_HoursAI','FreeTextClean']
analysis_cols += [c for c in clean.columns if c.startswith('Major_')]
analysis_ready = clean[analysis_cols].sort_values('id')
display(analysis_ready)
print("\n—— 清洗日志 ——")
print("\n".join(log))
| id | Name | Gender | Gender_le | age | JoinDate | Q1_Satisfaction | Q2_HoursAI | FreeTextClean | Major_历史学 | Major_文学 | Major_社会学 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 张三 | 男 | 1 | 20.0 | 2025-03-01 | 5.0 | 2.000 | 课程很好! | False | False | True |
| 1 | 2 | 李四 | 女 | 0 | 22.0 | NaT | 4.0 | 3.000 | 讲解清晰 | True | False | False |
| 2 | 3 | 王五 | 女 | 0 | 21.0 | 2025-03-05 | 3.0 | 1.000 | 内容太难;希望更多例子 | False | True | False |
| 3 | 4 | 赵六 | 男 | 1 | 21.0 | 2025-03-08 | 3.0 | 2.000 | 老师讲得不错 | False | False | True |
| 4 | 5 | 周七 | 男 | 1 | 21.0 | NaT | 5.0 | 3.875 | 很好 | True | False | False |
| 5 | 5 | 周七 | 男 | 1 | 21.0 | 2025-03-10 | 5.0 | 2.000 | 很好 | True | False | False |
—— 清洗日志 —— 类型转换:Q2_HoursAI->数值;JoinDate->日期;Major->分类 缺失/异常:总缺失从 2 降到 2 文本清洗:生成 FreeTextClean(strip/多空白合一/标点统一) 编码:Gender->LabelEncoding;Major->One-Hot
练习:把你的真实问卷或文本语料进行相同流程的清洗,并把每一步写入“清洗日志”。