嘿,朋友!欢迎来到数据分析的进阶之旅。我知道你可能正盯着屏幕上那些凌乱的数据发愁,或者看着报错信息怀疑人生。别担心,我也曾经经历过那个阶段——从只会 print() 到能熟练处理百万级数据,这条路我走过,而且走得挺稳的。今天,我们不讲枯燥的教科书定义,而是像老朋友聊天一样,带你一步步拆解数据分析的核心技能树。准备好了吗?我们要开始填坑了。
第一步:别急着跑模型,先把“脏数据”洗干净
很多人一上来就想用 Pandas 做 df.groupby() 或者 model.fit(),结果发现结果全是 NaN,或者图表乱成一团。真相是:80% 的时间你都在跟数据清洗打交道,只有 20% 的时间在做建模和可视化。 如果你跳过这一步,后面所有的努力都是建立在沙堆上的城堡。
1.1 认识你的数据:探索性数据分析(EDA)
在清洗之前,你得先“看一眼”你的数据。不是用 head() 随便瞄一眼,而是要像侦探一样审视它的每一个细节。
import pandas as pd
import numpy as np
# 假设我们有一个电商销售数据集
df = pd.read_csv('sales_data.csv')
# 1. 形状检查:有多少行、多少列?
print(f"数据形状: {df.shape}")
# 2. 数据类型:每列是什么类型?(这对后续处理至关重要)
print(df.dtypes)
# 3. 基本信息:统计分布、缺失值情况
print(df.info())
# 4. descriptive statistics:数值列的统计摘要
print(df.describe())
# 5. 唯一值检查:分类变量有多少种类别?
print(df['category'].nunique())
print(df['category'].value_counts())
为什么这很重要? 如果你发现 price 列里有负数,或者 date 列格式五花八门,这时候你就知道该往哪个方向挖了。
1.2 缺失值处理:是删是补,看情况而定
缺失值(NaN)是数据分析中的头号敌人。但别急着直接删除!删除可能会引入偏差,尤其是当缺失不是随机的(MNAR)时候。
策略一:删除 如果缺失比例极高(比如超过 50%),且对分析目标影响不大,直接删除该列。
# 删除缺失率超过 50% 的列
drop_cols = [col for col in df.columns if df[col].isnull().mean() > 0.5]
df = df.drop(columns=drop_cols)
策略二:填充(Imputation)
- 数值型: 用均值、中位数或众数填充。
- 如果数据分布均匀,用均值。
- 如果有极端值(如收入数据),用中位数更稳健。
- 如果某个类别只有一个主要值,用众数。
- 分类型: 用众数填充,或者新建一个“未知”类别。
- 高级填充: 用模型预测缺失值(如 KNN 填充),但这在初期可能有点过度设计。
# 数值列:用中位数填充
for col in ['age', 'income']:
df[col].fillna(df[col].median(), inplace=True)
# 分类列:用众数填充
df['gender'].fillna(df['gender'].mode()[0], inplace=True)
# 或者,把缺失值作为一个独立类别(适合某些机器学习模型)
df['product_category'].fillna('Unknown', inplace=True)
1.3 异常值检测:别让极端值带偏节奏
异常值可能是错误数据,也可能是真正的洞察。比如,一个用户单笔消费 100 万,可能是测试数据,也可能是大客户。
方法一:IQR 法则(四分位距) 这是最经典的方法。下界 = Q1 - 1.5*IQR,上界 = Q3 + 1.5*IQR。
def remove_outliers_iqr(df, column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return df[(df[column] >= lower_bound) & (df[column] <= upper_bound)]
df_clean = remove_outliers_iqr(df, 'purchase_amount')
方法二:Z-Score 适用于正态分布数据。Z-Score > 3 或 < -3 通常视为异常值。
from scipy import stats
z_scores = np.abs(stats.zscore(df['height']))
df_no_outliers = df[z_scores < 3]
记住: 处理异常值后,一定要记录你的操作!以便后续复现和解释。
1.4 数据格式统一:字符串的陷阱
日期和时间是数据清洗中最头疼的部分之一。
# 解析日期,同时处理多种格式
df['order_date'] = pd.to_datetime(df['order_date'], format='mixed', dayfirst=False)
# 提取年月日特征
df['year'] = df['order_date'].dt.year
df['month'] = df['order_date'].dt.month
df['day_of_week'] = df['order_date'].dt.dayofweek
# 字符串清洗:去空格、转小写、移除特殊字符
df['customer_name'] = df['customer_name'].str.strip().str.lower()
df['email'] = df['email'].str.replace(r'[^\w@.]+', '', regex=True)
第二步:Pandas 高级技巧——从“会用”到“精通”
基础语法谁都会,但高手和普通用户的区别在于:能否用高效、优雅的方式处理复杂数据操作。
2.1 链式操作(Chaining):让代码像讲故事一样流畅
不要用中间变量堆砌代码,用 Pandas 的 .pipe() 或链式调用让逻辑更清晰。
# 普通写法
temp_df = df[df['age'] > 18]
temp_df = temp_df[temp_df['income'] > 3000]
temp_df = temp_df.dropna(subset=['email'])
result = temp_df.groupby('city')['income'].mean()
# 链式写法:清晰、可读性强
result = (df
.query('age > 18 and income > 3000')
.dropna(subset=['email'])
.groupby('city')['income']
.mean()
.sort_values(ascending=False))
2.2 Apply 与矢量化:性能优化的关键
apply 函数很方便,但它是 Python 级别的循环,数据量大时非常慢。优先考虑向量化操作!
# 慢:用 apply 计算折扣价
df['discounted_price'] = df['price'].apply(lambda x: x * 0.8 if x > 100 else x)
# 快:用向量化操作
df['discounted_price'] = np.where(df['price'] > 100, df['price'] * 0.8, df['price'])
当必须用 apply 时,尝试传入 axis=1 并优化 lambda 函数,或者使用 numba 加速。
2.3 分组聚合的进阶:transform 和 apply
transform:返回与原数据相同长度的 Series,常用于填充分组统计值。apply:返回任意形状,用于复杂逻辑。
# 场景:为每个城市的价格添加“该城市平均价格”列
df['city_avg_price'] = df.groupby('city')['price'].transform('mean')
# 场景:自定义复杂聚合
def custom_agg(group):
return pd.Series({
'mean_price': group['price'].mean(),
'std_price': group['price'].std(),
'count': group['price'].count()
})
result = df.groupby('city').apply(custom_agg)
2.4 合并数据:merge, join, concat
数据往往分散在多个表中,学会高效合并是关键。
# 内连接:只保留匹配的行
merged_df = pd.merge(df_orders, df_customers, on='customer_id', how='inner')
# 左连接:保留左表所有行
left_merged = pd.merge(df_orders, df_products, on='product_id', how='left')
# 横向合并:按索引拼接(如添加新特征列)
df_with_features = pd.concat([df_base, df_features], axis=1)
第三步:可视化——让数据“说话”
好的可视化不是花哨,而是清晰传达信息。Matplotlib 强大但繁琐,Seaborn 简洁美观,Plotly 交互性强。我建议:日常分析用 Seaborn,做报告或展示用 Plotly。
3.1 Seaborn:统计绘图的利器
import seaborn as sns
import matplotlib.pyplot as plt
# 设置风格
sns.set_style("whitegrid")
# 1. 分布图:看单变量分布
plt.figure(figsize=(10, 6))
sns.histplot(df['purchase_amount'], kde=True, bins=30)
plt.title('Purchase Amount Distribution')
plt.show()
# 2. 箱线图:看异常值和分布
plt.figure(figsize=(10, 6))
sns.boxplot(x='category', y='price', data=df)
plt.title('Price Distribution by Category')
plt.show()
# 3. 热力图:看相关性
plt.figure(figsize=(8, 6))
sns.heatmap(df.corr(numeric_only=True), annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Correlation Matrix')
plt.show()
# 4. 联合分布图:看两个变量的关系
plt.figure(figsize=(8, 6))
sns.jointplot(data=df, x='age', y='income', kind='kde', fill=True)
plt.show()
3.2 Plotly:交互式可视化
当数据量大或需要嵌入网页时,Plotly 是不二之选。
import plotly.express as px
# 散点图:可缩放、可悬停查看细节
fig = px.scatter(df, x='age', y='income', color='gender', size='purchase_amount',
hover_data=['product_category'], title='Age vs Income by Gender')
fig.show()
# 树状图:展示层级结构数据
fig = px.treemap(df, path=['category', 'subcategory'], values='sales',
title='Sales Breakdown by Category')
fig.show()
第四步:特征工程——建模的灵魂
特征工程的好坏,直接决定模型上限。这一节我们讲一些实战技巧。
4.1 数值特征处理
- 标准化(Standardization): 减去均值,除以标准差。适用于 SVM、逻辑回归、神经网络。
- 归一化(Normalization): 缩放到 [0, 1] 区间。适用于图像数据、KNN。
- 分箱(Binning): 将连续变量离散化,增加鲁棒性。
from sklearn.preprocessing import StandardScaler, MinMaxScaler, KBinsDiscretizer
# 标准化
scaler = StandardScaler()
df['age_scaled'] = scaler.fit_transform(df[['age']])
# 分箱:将年龄分为 3 个区间
binner = KBinsDiscretizer(n_bins=3, encode='ordinal', strategy='uniform')
df['age_bin'] = binner.fit_transform(df[['age']])
4.2 分类特征处理
- 独热编码(One-Hot Encoding): 适用于低基数分类变量(类别少)。
- 标签编码(Label Encoding): 适用于有序变量,或树模型。
- 目标编码(Target Encoding): 适用于高基数分类变量(如用户 ID),用目标均值替换类别。
from sklearn.preprocessing import OneHotEncoder
# 独热编码
ohe = OneHotEncoder(sparse_output=False, handle_unknown='ignore')
encoded_categories = ohe.fit_transform(df[['product_category']])
encoded_df = pd.DataFrame(encoded_categories, columns=ohe.get_feature_names_out(['product_category']))
df = pd.concat([df, encoded_df], axis=1)
# 目标编码(手动实现)
mean_by_cat = df.groupby('product_category')['sales'].mean()
df['sales_encoded'] = df['product_category'].map(mean_by_cat)
4.3 特征选择:去掉噪音,留下精华
- 方差阈值: 删除低方差特征(几乎不变的特征)。
- 相关性过滤: 删除与目标变量相关性低的特征。
- 模型重要性: 用树模型(如随机森林)的
feature_importances_筛选。
from sklearn.feature_selection import VarianceThreshold, SelectKBest, f_classif
# 方差阈值
selector = VarianceThreshold(threshold=0.1)
var_filtered = selector.fit_transform(df.drop('target', axis=1))
# 卡方检验选择最佳 K 个特征
selector_kbest = SelectKBest(score_func=f_classif, k=10)
kbest_selected = selector_kbest.fit_transform(df.drop('target', axis=1), df['target'])
第五步:机器学习建模——从理论到实践
选对模型,只是成功的一半;调参和验证,才是决定成败的关键。
5.1 模型选择指南
- 回归问题:
- 线性回归(基线模型)
- 正则化回归(Lasso/Ridge,防止过拟合)
- 树模型(Random Forest, XGBoost, LightGBM,通常表现最佳)
- 分类问题:
- 逻辑回归(基线)
- SVM(小数据集,高维)
- 树模型集成(XGBoost, LightGBM,首选)
- 神经网络(大数据集,图像/文本)
5.2 交叉验证:避免过拟合的神器
不要只用一次 train-test split!用 K 折交叉验证(K-Fold CV)来评估模型稳定性。
from sklearn.model_selection import cross_val_score, KFold
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_breast_cancer
# 加载数据
data = load_breast_cancer()
X, y = data.data, data.target
# 初始化模型
model = RandomForestClassifier(random_state=42)
# K 折交叉验证
kfold = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=kfold, scoring='accuracy')
print(f"Cross-Validation Scores: {scores}")
print(f"Mean Accuracy: {scores.mean():.4f} (+/- {scores.std() * 2:.4f})")
5.3 超参数调优:Grid Search 与 Random Search
- Grid Search: 穷举所有参数组合,精度高但慢。
- Random Search: 随机采样参数组合,效率高,通常能接近 Grid Search 的结果。
- Optuna: 更智能的贝叶斯优化,推荐用于复杂调参。
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from sklearn.ensemble import GradientBoostingClassifier
import numpy as np
model = GradientBoostingClassifier(random_state=42)
# 定义参数网格
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [3, 5, 7],
'learning_rate': [0.01, 0.1, 0.2]
}
# 随机搜索(比网格搜索快得多)
random_search = RandomizedSearchCV(
estimator=model,
param_distributions=param_grid,
n_iter=10,
cv=3,
verbose=1,
random_state=42,
n_jobs=-1
)
random_search.fit(X_train, y_train)
print(f"Best Parameters: {random_search.best_params_}")
print(f"Best CV Score: {random_search.best_score_:.4f}")
5.4 模型评估:不只是看准确率
对于不平衡数据集,准确率会骗人!
- 混淆矩阵: 看 TP, TN, FP, FN。
- 分类报告: 精确率(Precision)、召回率(Recall)、F1-Score。
- ROC-AUC: 衡量模型整体区分能力。
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
import seaborn as sns
import matplotlib.pyplot as plt
# 预测
y_pred = random_search.predict(X_test)
y_prob = random_search.predict_proba(X_test)[:, 1]
# 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('Predicted')
plt.ylabel('True')
plt.title('Confusion Matrix')
plt.show()
# 分类报告
print(classification_report(y_test, y_pred))
# ROC-AUC
print(f"ROC-AUC Score: {roc_auc_score(y_test, y_prob):.4f}")
第六步:实战案例解析——房价预测完整流程
理论讲完了,我们来一个完整的实战案例:使用波士顿房价数据集(或类似数据),从清洗到建模全流程演示。
6.1 环境准备与数据加载
”`python import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split, cross_val_score from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score import
