机器学习基础
▶ 什么是机器学习?
机器学习(Machine Learning)是人工智能的一个分支,通过算法让计算机从数据中学习规律,从而对未知数据进行预测或决策,而无需显式编程。
核心思想:
传统编程 vs 机器学习对比:
| 对比维度 | 传统编程 | 机器学习 |
|---|---|---|
| 输入 | 规则 + 数据 | 数据 + 结果(标签) |
| 输出 | 结果 | 规则(模型) |
| 规则来源 | 人工定义 | 从数据中学习 |
| 适应性 | 固定规则,难以应对变化 | 自动调整,适应新数据 |
| 复杂问题处理 | 规则难以穷尽 | 可自动发现复杂规律 |
| 典型应用 | 计算器、库存管理 | 图像识别、推荐系统 |
机器学习的三要素:
数据(Data):学习的原材料
- 特征(Features):描述样本的属性
- 标签(Labels):监督学习中的目标值
模型(Model):学习的算法
- 决定如何从数据中学习规律
- 如:线性回归、决策树、神经网络
目标函数(Objective Function):优化的目标
- 损失函数:衡量预测与真实值的差距
- 优化目标:最小化损失
机器学习的工作流程:
1. 数据收集
↓
2. 数据预处理(清洗、特征工程)
↓
3. 选择模型
↓
4. 训练模型(学习参数)
↓
5. 模型评估
↓
6. 模型优化/调参
↓
7. 模型部署应用
▶ 机器学习的三大类问题是什么?
机器学习根据任务类型和数据特点,可以分为三大类:监督学习、无监督学习和强化学习。
1. 监督学习(Supervised Learning)
特点:
- 训练数据有标签(label)
- 目标是学习从输入到输出的映射
分类(Classification)
预测离散的类别:
常见任务:
- 垃圾邮件识别(垃圾/正常)
- 用户流失预测(流失/不流失)
- 图像识别(猫/狗/鸟…)
常用算法:
- 逻辑回归
- 决策树
- 随机森林
- 支持向量机(SVM)
- 神经网络
评估指标:
- 准确率(Accuracy)
- 精确率(Precision)
- 召回率(Recall)
- F1-Score
- AUC-ROC
回归(Regression)
预测连续的数值:
常见任务:
- 房价预测
- 销售额预测
- 用户LTV预测
常用算法:
- 线性回归
- 多项式回归
- 岭回归/Lasso回归
- 决策树回归
- 神经网络
评估指标:
- MSE(均方误差)
- RMSE(均方根误差)
- MAE(平均绝对误差)
- R²(决定系数)
2. 无监督学习(Unsupervised Learning)
特点:
- 训练数据没有标签
- 目标是发现数据的内在结构和模式
常见任务:
聚类(Clustering):将相似样本分组
- 用户分群(RFM)
- 商品分类
- 异常检测
降维(Dimensionality Reduction):减少特征数量
- 数据可视化
- 特征提取
- 去噪
常用算法:
- K-Means聚类
- 层次聚类
- DBSCAN
- PCA(主成分分析)
- t-SNE
3. 强化学习(Reinforcement Learning)
特点:
- 智能体(Agent)与环境交互
- 通过奖励反馈学习最优策略
- 目标是最大化长期累积奖励
常见应用:
- 游戏AI(AlphaGo)
- 机器人控制
- 推荐系统
- 自动驾驶
常用算法:
- Q-Learning
- Deep Q-Network(DQN)
- Policy Gradient
- Actor-Critic
如何选择学习类型
有标签数据 → 监督学习
- 分类:离散目标(类别)
- 回归:连续目标(数值)
无标签数据 → 无监督学习
- 聚类:分组相似样本
- 降维:简化数据结构
需要序列决策 → 强化学习
实际项目中:
- 数据分析岗位主要用监督学习和无监督学习
- 强化学习在推荐系统、游戏等特定领域应用
常用算法
▶ K-Means聚类是什么?原理和应用场景?
K-Means是最常用的聚类算法,通过迭代优化将数据分为K个簇,使簇内样本相似度高、簇间相似度低。
算法原理:
基本流程
初始化:随机选择K个样本作为初始聚类中心
分配:计算每个样本到K个中心的距离,分配到最近的簇
更新:重新计算每个簇的中心(簇内样本的均值)
迭代:重复步骤2-3,直到中心不再变化或达到最大迭代次数
示例(K=2):
初始化:随机选2个中心点
迭代1:
- 分配:每个点归属最近的中心
- 更新:重新计算2个簇的中心
迭代2:
- 分配:重新分配
- 更新:重新计算中心
...
收敛:中心不再移动
数学原理
目标函数(最小化簇内平方和):
$$ J = \sum_{i=1}^{K} \sum_{x \in C_i} ,,x - \mu_i,,^2 $$
其中:
- $K$:簇的数量
- $C_i$:第i个簇
- $μ_i$:第i个簇的中心
- $||x - μ_i||$:样本x到中心的欧氏距离
距离计算(欧氏距离):
$$ d(x, y) = \sqrt{\sum_{i=1}^{n}(x_i - y_i)^2} $$
优缺点分析
优点:
- 简单易实现
- 计算效率高
- 适合大数据集
- 效果通常不错
缺点:
- 需要预先指定K值
- 对初始中心敏感(可能陷入局部最优)
- 只能发现球形簇(圆形/椭圆形)
- 对异常值敏感
改进方法:
- K-Means++:改进初始化方法
- Mini-Batch K-Means:适合超大数据集
- 多次运行取最优结果
如何确定K值:
肘部法则(Elbow Method):
- 绘制K与簇内平方和(SSE)的关系曲线
- 找到曲线的"肘部"(拐点)
- 拐点对应的K值较合理
轮廓系数(Silhouette Score):
- 衡量样本与所属簇的相似度
- 取值-1到1,越接近1越好
- 选择轮廓系数最高的K值
业务经验:
- 根据业务需求决定分几类
- 如:用户分群可能分为3-5类
实际应用场景:
用户分群:
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# 用户RFM数据
rfm = df[['recency', 'frequency', 'monetary']]
# 标准化
scaler = StandardScaler()
rfm_scaled = scaler.fit_transform(rfm)
# K-Means聚类
kmeans = KMeans(n_clusters=4, random_state=42)
df['cluster'] = kmeans.fit_predict(rfm_scaled)
# 分析每个簇的特征
df.groupby('cluster')[['recency', 'frequency', 'monetary']].mean()
商品分类:
- 基于商品属性(价格、销量、评分等)聚类
- 发现相似商品群组
- 用于推荐、定价策略
异常检测:
- 正常样本聚为一类
- 远离所有簇中心的样本可能是异常值
K-Means使用注意事项
数据标准化:
- K-Means基于距离,不同量纲的特征会影响结果
- 使用前必须标准化(StandardScaler)
处理异常值:
- K-Means对异常值敏感
- 建议先处理明显的异常值
特征选择:
- 选择对分群有意义的特征
- 去除冗余特征
结果解释:
- 聚类结果需要结合业务解释
- 分析每个簇的特征统计
▶ 决策树是什么?有什么作用和原理?
决策树是一种基于树形结构进行决策的监督学习算法,通过一系列if-then规则对数据进行分类或回归。
决策树的直观理解:
就像一个连续提问的流程图:
[年龄 < 30?]
/ \
是 否
/ \
[收入 < 5万?] [资产 > 100万?]
/ \ / \
拒绝 通过 拒绝 通过
决策树的组成:
- 根节点:最顶层的节点,包含所有样本
- 内部节点:代表一个特征的判断
- 叶节点:最终的决策结果(分类/数值)
- 分支:决策的路径
算法原理:
分裂标准
如何选择最优的分裂特征和分裂点?
分类树常用指标:
- 信息增益(Information Gain) - ID3算法
$$ IG(D, A) = Entropy(D) - \sum_{v} \frac{,D_v,}{,D,} Entropy(D_v) $$
- 选择信息增益最大的特征
- 偏向取值多的特征
信息增益率(Gain Ratio) - C4.5算法
- 改进信息增益的偏向问题
基尼系数(Gini Index) - CART算法
$$ Gini(D) = 1 - \sum_{k=1}^{K} p_k^2 $$
- 选择基尼系数下降最多的特征
- 计算速度快
回归树常用指标:
平方误差(MSE):选择MSE下降最多的分裂点
构建过程
递归构建决策树:
function BuildTree(数据集D):
if 停止条件满足:
return 叶节点
找到最优分裂特征A和分裂点v
创建节点:
左子树 = BuildTree(D中A≤v的样本)
右子树 = BuildTree(D中A>v的样本)
return 当前节点
停止条件:
- 所有样本属于同一类别
- 没有更多特征可用
- 达到最大深度
- 节点样本数低于阈值
剪枝(Pruning)
防止过拟合的关键步骤:
预剪枝(Pre-pruning):
- 构建时设置停止条件
- 如:最大深度、最小样本数
- 优点:简单高效
- 缺点:可能欠拟合
后剪枝(Post-pruning):
- 先生成完整树
- 再从下往上剪枝
- 根据验证集性能决定是否剪枝
- 优点:效果更好
- 缺点:计算成本高
决策树的优缺点:
优点:
- 易于理解和解释,可视化直观
- 不需要数据标准化
- 可以处理数值和类别特征
- 可以处理缺失值
- 非参数模型,无需假设数据分布
- 特征重要性可解释
缺点:
- 容易过拟合(需要剪枝)
- 对数据敏感,小变化可能导致树结构大变
- 倾向于过拟合训练数据
- 对不平衡数据敏感
- 单棵树性能有限
应用场景:
- 信用评分:判断是否给予贷款
- 医疗诊断:根据症状诊断疾病
- 客户流失预测:预测客户是否会流失
- 推荐系统:根据用户特征推荐商品
实际代码示例:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn import tree
import matplotlib.pyplot as plt
# 准备数据
X = df[['age', 'income', 'balance']]
y = df['churn']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# 训练决策树
clf = DecisionTreeClassifier(
max_depth=3, # 最大深度(预剪枝)
min_samples_split=20, # 分裂所需最小样本数
min_samples_leaf=10, # 叶节点最小样本数
random_state=42
)
clf.fit(X_train, y_train)
# 预测
y_pred = clf.predict(X_test)
# 可视化决策树
plt.figure(figsize=(12, 8))
tree.plot_tree(clf, feature_names=X.columns,
class_names=['Not Churn', 'Churn'],
filled=True, rounded=True)
plt.show()
# 特征重要性
feature_importance = pd.DataFrame({
'feature': X.columns,
'importance': clf.feature_importances_
}).sort_values('importance', ascending=False)
决策树调参建议
关键参数:
max_depth:树的最大深度
- 较小:欠拟合风险
- 较大:过拟合风险
- 建议:从3-10开始尝试
min_samples_split:分裂所需最小样本数
- 增大可防止过拟合
- 建议:10-50
min_samples_leaf:叶节点最小样本数
- 增大可防止过拟合
- 建议:5-20
max_features:寻找最优分裂时考虑的最大特征数
- 增加随机性,防止过拟合
- 建议:‘sqrt’或’log2’
▶ 什么是过拟合和欠拟合?如何解决?
过拟合和欠拟合是机器学习模型性能的两个极端状态,需要找到平衡点。
欠拟合(Underfitting):
定义:模型过于简单,无法捕捉数据的规律
表现:
- 训练集误差高
- 测试集误差高
- 训练集和测试集误差接近
原因:
- 模型太简单(如用线性模型拟合非线性数据)
- 特征太少,信息不足
- 正则化过度
解决方法:
- 使用更复杂的模型
- 增加特征
- 减小正则化强度
- 增加训练时间
过拟合(Overfitting):
定义:模型过于复杂,学习了数据的噪声而不是真实规律
表现:
- 训练集误差很低
- 测试集误差高
- 训练集和测试集误差差距大
原因:
- 模型太复杂(参数过多)
- 训练数据太少
- 训练时间过长
- 没有正则化
解决方法:
数据层面
增加训练数据:
- 收集更多数据
- 数据增强(图像翻转、文本同义替换等)
特征选择:
- 去除不相关特征
- 降维(PCA)
数据清洗:
- 去除异常值和噪声
模型层面
降低模型复杂度:
- 减少参数数量
- 降低多项式次数
- 减少神经网络层数/节点数
正则化:
- L1正则化(Lasso):稀疏化,特征选择
- L2正则化(Ridge):参数平滑化
- Dropout(神经网络):随机丢弃节点
决策树剪枝:
- 限制max_depth
- 设置min_samples_split/leaf
训练层面
Early Stopping:
- 监控验证集误差
- 验证集误差不再下降时停止训练
交叉验证:
- K-Fold交叉验证
- 更好地评估泛化性能
正则化参数调整:
- 增大正则化系数α/λ
集成方法
Bagging:
- 随机森林
- 减少方差,防止过拟合
Boosting:
- XGBoost、LightGBM
- 逐步修正错误
如何判断过拟合/欠拟合:
学习曲线分析:
欠拟合:
训练误差 ───────── 高且平
测试误差 ───────── 高且平
良好拟合:
训练误差 ╲ 低
测试误差 ───── 低且稳定
过拟合:
训练误差 ╲╲ 很低
测试误差 ╱─ 较高且不稳定
实际案例:
from sklearn.model_selection import learning_curve
import numpy as np
import matplotlib.pyplot as plt
# 绘制学习曲线
def plot_learning_curve(model, X, y):
train_sizes, train_scores, test_scores = learning_curve(
model, X, y, cv=5,
train_sizes=np.linspace(0.1, 1.0, 10)
)
train_mean = np.mean(train_scores, axis=1)
train_std = np.std(train_scores, axis=1)
test_mean = np.mean(test_scores, axis=1)
test_std = np.std(test_scores, axis=1)
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_mean, label='Training score')
plt.plot(train_sizes, test_mean, label='Validation score')
plt.fill_between(train_sizes, train_mean - train_std,
train_mean + train_std, alpha=0.1)
plt.fill_between(train_sizes, test_mean - test_std,
test_mean + test_std, alpha=0.1)
plt.xlabel('Training Set Size')
plt.ylabel('Score')
plt.legend()
plt.title('Learning Curve')
plt.show()
# 使用
plot_learning_curve(model, X_train, y_train)
偏差-方差权衡
总误差 = 偏差² + 方差 + 不可约误差
偏差(Bias):模型的预测值与真实值的差距
- 高偏差 → 欠拟合
- 模型太简单
方差(Variance):模型对训练数据变化的敏感程度
- 高方差 → 过拟合
- 模型太复杂
目标:在偏差和方差之间找到平衡点
▶ 什么是硬距离和软距离?(在聚类中)
硬距离和软距离是聚类算法中两种不同的样本分配方式。
硬聚类(Hard Clustering)/ 硬距离:
定义:每个样本明确属于某一个簇,非此即彼
特点:
- 样本只属于一个簇
- 簇的归属是确定的(0或1)
- 边界清晰
典型算法:
- K-Means
- 层次聚类
- DBSCAN
示例:
样本A:
簇1: 0 (不属于)
簇2: 1 (属于)
簇3: 0 (不属于)
软聚类(Soft Clustering)/ 软距离:
定义:每个样本以一定概率属于多个簇
特点:
- 样本可以部分属于多个簇
- 簇的归属是概率分布
- 边界模糊
典型算法:
- GMM(高斯混合模型)
- Fuzzy C-Means(模糊C均值)
示例:
样本A:
簇1: 0.1 (10%概率属于)
簇2: 0.7 (70%概率属于)
簇3: 0.2 (20%概率属于)
概率和 = 1.0
对比:
, 维度 , 硬聚类 , 软聚类 , ,——,——–,——–, , 归属方式 , 确定(0/1) , 概率(0-1) , , 边界 , 清晰 , 模糊 , , 计算复杂度 , 较低 , 较高 , , 适用场景 , 类别明确 , 类别重叠 , , 结果解释 , 简单直观 , 更丰富但复杂 ,
应用场景选择:
硬聚类适用于:
- 类别界限分明的场景
- 用户明确分为"高价值"和"低价值"
- 商品明确分为不同类目
- 需要简单明确的结果
- 计算资源有限
软聚类适用于:
- 类别边界模糊的场景
- 用户可能同时具有多种属性
- 文档可能涉及多个主题
- 需要了解不确定性
- 后续需要概率信息
Fuzzy C-Means示例:
import skfuzzy as fuzz
import numpy as np
# 数据
data = np.array([[1, 2], [1.5, 1.8], [5, 8], [8, 8], [1, 0.6], [9, 11]])
# Fuzzy C-Means聚类
cntr, u, u0, d, jm, p, fpc = fuzz.cluster.cmeans(
data.T, # 转置
c=2, # 簇数量
m=2, # 模糊度参数
error=0.005,
maxiter=1000
)
# u 是隶属度矩阵(每行是一个样本,每列是一个簇)
# u[0, :] 第一个样本对各个簇的隶属度
print("样本1的隶属度:", u[:, 0]) # [0.95, 0.05] 表示95%属于簇1,5%属于簇2
硬聚类vs软聚类选择建议
数据分析岗位实际应用:
用户分群(RFM):通常用硬聚类(K-Means)
- 需要明确的用户类别
- 便于制定运营策略
文本主题分类:可以用软聚类
- 文本可能涉及多个主题
- 需要主题概率分布
商品推荐:可以结合使用
- 硬聚类:明确的商品分类
- 软聚类:用户兴趣的概率分布
实践建议:
- 先用硬聚类快速探索
- 如果发现边界模糊问题,再考虑软聚类
- 根据业务需求决定是否需要概率信息
模型评估
▶ 分类模型如何评估?常用指标有哪些?
分类模型的评估需要根据业务场景选择合适的指标。
混淆矩阵(Confusion Matrix):
分类评估的基础:
预测
正例 负例
实际 正例 TP FN
负例 FP TN
- TP(True Positive):真阳性,预测为正且实际为正
- FP(False Positive):假阳性,预测为正但实际为负
- TN(True Negative):真阴性,预测为负且实际为负
- FN(False Negative):假阴性,预测为负但实际为正
常用评估指标:
准确率(Accuracy)
定义:预测正确的样本占总样本的比例
$$ Accuracy = \frac{TP + TN}{TP + TN + FP + FN} $$
优点:简单直观
缺点:
- 类别不平衡时会误导
- 如:99%负例,1%正例,全预测为负也能达到99%准确率
适用场景:类别平衡的二分类问题
精确率&召回率
精确率(Precision):
预测为正例中真正的正例比例
$$ Precision = \frac{TP}{TP + FP} $$
含义:预测为正的样本中有多少是对的
召回率(Recall)/ 灵敏度:
实际正例中被正确预测的比例
$$ Recall = \frac{TP}{TP + FN} $$
含义:实际正例中有多少被找出来了
权衡:
- 提高精确率 → 召回率降低(更保守)
- 提高召回率 → 精确率降低(更激进)
F1-Score
精确率和召回率的调和平均数:
$$ F1 = 2 \times \frac{Precision \times Recall}{Precision + Recall} $$
或更一般的F_β Score:
$$ F_β = (1 + β^2) \times \frac{Precision \times Recall}{β^2 \times Precision + Recall} $$
- β<1:更重视精确率
- β>1:更重视召回率
- β=1:F1-Score,平衡两者
适用场景:
- 类别不平衡
- 需要平衡精确率和召回率
ROC曲线&AUC
ROC曲线(Receiver Operating Characteristic):
横轴:FPR(假阳性率) 纵轴:TPR(真阳性率/召回率)
绘制:改变分类阈值,得到不同的(FPR, TPR)点连成的曲线
AUC(Area Under Curve):
ROC曲线下的面积,取值0-1:
- AUC = 1:完美分类器
- AUC = 0.5:随机猜测
- AUC < 0.5:比随机还差
优点:
- 不受类别不平衡影响
- 衡量模型整体性能
- 易于比较不同模型
如何选择评估指标:
根据业务场景选择:
场景1:垃圾邮件识别
重视:Precision(精确率)
原因:
- 误判正常邮件为垃圾邮件代价高
- 宁可漏掉一些垃圾邮件,也不能误判正常邮件
场景2:疾病诊断
重视:Recall(召回率)
原因:
- 漏诊代价极高(可能危及生命)
- 误诊可以通过进一步检查纠正
场景3:信用评分
重视:AUC
原因:
- 需要排序能力(对申请人排序)
- 不关心具体阈值
- 整体性能更重要
场景4:用户流失预测
重视:F1-Score
原因:
- 需要平衡:既要找到流失用户,又不能骚扰忠诚用户
- 类别可能不平衡
实际代码示例:
from sklearn.metrics import (
confusion_matrix, classification_report,
roc_auc_score, roc_curve
)
import matplotlib.pyplot as plt
# 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
print("混淆矩阵:\n", cm)
# 分类报告(包含precision, recall, f1-score)
print(classification_report(y_test, y_pred))
# AUC
auc = roc_auc_score(y_test, y_pred_proba)
print(f"AUC: {auc:.3f}")
# ROC曲线
fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba)
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, label=f'ROC (AUC = {auc:.3f})')
plt.plot([0, 1], [0, 1], 'k--', label='Random')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.show()
评估指标使用注意事项
类别不平衡时:
- 不要只看Accuracy
- 重点关注Precision、Recall、F1、AUC
业务优先:
- 根据业务成本选择指标
- 误判的代价是什么?
综合评估:
- 不要只看单一指标
- 结合多个指标全面评估
交叉验证:
- 单次评估可能有偶然性
- 使用K-Fold交叉验证更可靠
▶ 介绍一下你做过的Steam黑神话悟空游戏评论分析项目
这是一个基于Steam平台3626条《黑神话·悟空》简体中文评论数据的综合分析项目,主要包含以下几个部分:
数据采集:
- 使用Python爬虫(Selenium)从Steam平台爬取游戏评论数据
- 采集了评论内容、推荐状态、游戏时长、发布时间等关键字段
- 处理了动态加载、反爬虫等技术挑战
数据分析维度:
- 口碑分析:推荐率71.15%,正面情感占比58.88%,整体口碑良好
- 用户画像:分析了玩家游戏时长分布,平均游戏时长54小时
- 内容分析:通过词频分析和情感分析,发现玩家对剧情、战斗系统、文化元素关注度高
- 时间趋势:分析评论量随时间的变化趋势
技术栈:
- 数据采集:Python + Selenium
- 数据处理:Pandas + NumPy
- 文本分析:jieba分词 + SnowNLP情感分析
- 可视化:ECharts + Matplotlib
项目价值: 为游戏运营团队提供了用户口碑洞察,帮助识别玩家关注的核心要素和潜在问题点
▶ 电商用户行为分析项目中,你是如何进行用户分群的?
这个项目基于1000条电商用户数据,采用RFM模型和K-Means聚类算法进行用户分群:
RFM模型应用:
- R (Recency):最近一次购买时间,反映用户活跃度
- F (Frequency):购买频次,反映用户忠诚度
- M (Monetary):消费金额,反映用户价值
分群步骤:
数据准备阶段
- 清洗用户行为数据,去除异常值和重复数据
- 统一时间格式和金额单位
- 确保每个用户有完整的交易记录
RFM指标计算
# 计算RFM值
current_date = df['date'].max()
rfm = df.groupby('user_id').agg({
'date': lambda x: (current_date - x.max()).days, # R
'order_id': 'count', # F
'amount': 'sum' # M
})
- R值:距离最后一次购买的天数(值越小越好)
- F值:总购买次数
- M值:总消费金额
K-Means聚类
- 对RFM三个维度进行标准化处理
- 使用肘部法则确定最优聚类数(K=4)
- 应用K-Means算法进行聚类
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# 标准化
scaler = StandardScaler()
rfm_scaled = scaler.fit_transform(rfm)
# K-Means聚类
kmeans = KMeans(n_clusters=4, random_state=42)
rfm['cluster'] = kmeans.fit_predict(rfm_scaled)
业务应用
根据聚类结果,将用户分为四类:
| 用户类型 | RFM特征 | 用户特点 | 营销策略 | 预期效果 |
|---|---|---|---|---|
| 高价值用户 | R高、F高、M高 | 活跃且高价值 | VIP服务、专属权益、优先体验 | 维持忠诚度,提升LTV |
| 潜力用户 | R高、F低、M中 | 新用户或低频用户 | 提升购买频次、会员转化、积分激励 | 转化为高价值用户 |
| 重要挽留用户 | R低、F高、M高 | 流失风险的老客户 | 召回活动、大额优惠券、专人服务 | 重新激活,防止流失 |
| 一般用户 | 中等指标 | 普通用户 | 常规营销、品类推荐、活动推送 | 保持活跃,逐步提升 |
项目成果: 为不同用户群体制定了差异化营销策略,精准运营提升了整体转化效率
▶ AB测试项目中,如何确保实验的科学性?
在Web新功能点击率AB测试项目中,我遵循了完整的AB测试流程来确保实验科学性:
实验设计阶段:
- 明确假设:新功能设计能够提升用户点击率
- 核心指标:点击率(CTR)作为主要评估指标
- 样本量计算:
- 基于baseline点击率3.5%
- 期望检测到的最小提升10%
- 显著性水平α=0.05,统计功效1-β=0.8
- 计算得每组需要约6万样本
实验执行阶段:
关键控制点
随机分流:
- 采用Hash分流算法确保用户随机分配
- 验证对照组和实验组的用户特征分布一致(年龄、性别、地域等)
单一变量:
- 只改变一个变量(新功能设计)
- 其他条件完全一致
时间控制:
- 实验周期14天,覆盖两个完整周
- 避免节假日等特殊时期的干扰
数据埋点:
- 设计完整的埋点方案,记录曝光、点击、转化等关键事件
- 包含用户ID、实验组别、时间戳等必要字段
数据分析阶段:
- 假设检验:使用Mann-Whitney U检验(非参数检验,因为数据不符合正态分布)
- 统计显著性:p < 0.05,拒绝原假设
- 效应量评估:实验组点击率3.86% vs 对照组3.47%,提升11.52%
- 置信区间:计算95%置信区间,确认提升的稳定性
结果验证:
- 分时段验证(工作日vs周末)
- 分用户群验证(新用户vs老用户)
- 长期效果追踪(避免新奇效应)
最终结论: 实验组显著优于对照组(p<0.05),建议全量上线新功能
▶ 岗位数据分析项目中,你发现了哪些有价值的洞察?
这是基于鱼泡直聘284条数据分析岗位数据的分析项目,发现了以下关键洞察:
薪资水平洞察:
- 平均薪资14564元/月,中位数13000元/月
- 薪资分布呈右偏态,高薪岗位主要集中在一线城市
- 北上深杭平均薪资17000+,二线城市12000左右
技能需求洞察:
必备核心技能(出现频率>80%)
- SQL:数据查询和处理的基础,几乎所有岗位都要求
- Excel:数据透视、函数应用、图表制作
- Python:数据分析(Pandas、NumPy)、数据可视化
进阶技能(出现频率50-80%)
- 数据可视化工具:Tableau、Power BI、ECharts
- 统计分析:假设检验、AB测试、回归分析
- 业务理解:用户分析、漏斗分析、RFM模型
加分项(出现频率20-50%)
- 机器学习基础:聚类、分类、预测模型
- 大数据工具:Hive、Spark(大厂要求)
- 爬虫技能:数据采集能力
学历与经验要求:
- 本科学历占比70%,部分岗位接受优秀专科
- 1-3年经验需求占比最高(55%)
- 应届生岗位占比15%,但要求有项目经验
地域分布洞察:
- 一线城市岗位占比45%,新一线城市占比35%
- 深圳、杭州、上海岗位数量位列前三
- 二线城市也有较多机会,但薪资相对较低
行业分布:
- 互联网/电商行业需求最大(40%)
- 金融、教育、游戏行业也有较多需求
- 传统行业逐步重视数据分析
求职建议
基于以上洞察,对求职者的建议:
- 夯实基础:SQL、Excel、Python是必备技能,要达到熟练水平
- 项目经验:准备2-3个完整的数据分析项目,能够体现分析思路和业务价值
- 工具拓展:至少掌握一种BI工具(Tableau/PowerBI)
- 业务思维:关注业务场景,不只是技术层面的分析
- 持续学习:关注行业动态,了解AB测试、增长分析等进阶方法
▶ 在项目中遇到数据质量问题,你是如何处理的?
数据质量问题是数据分析项目中最常见的挑战,我通常采用以下系统化方法处理:
问题识别阶段:
完整性检查:
- 检查缺失值比例,超过30%的字段需要评估是否可用
- 确认关键字段(如user_id、timestamp)的完整性
准确性检查:
- 检查数值范围是否合理(如年龄-100到200明显异常)
- 逻辑一致性验证(如注册时间晚于最后登录时间)
一致性检查:
- 同一指标在不同数据源的值是否一致
- 数据口径是否统一
处理策略:
缺失值处理
根据缺失比例和业务含义选择策略:
- 删除:缺失比例<5%且为关键字段,直接删除
- 填充均值/中位数:数值型字段,缺失<20%
- 填充众数:类别型字段
- 标记为"未知":缺失本身有业务含义
- 模型预测:重要字段可用其他特征预测
# 示例
df['age'].fillna(df['age'].median(), inplace=True)
df['category'].fillna('unknown', inplace=True)
异常值处理
- 箱线图识别:IQR方法识别异常值
- 业务判断:结合业务逻辑判断是否为真实异常
- 处理方式:
- 删除:明显错误的数据
- 截断:将超出范围的值截断到边界
- 保留:合理的极端值
# 示例:将异常值截断到99分位数
Q99 = df['value'].quantile(0.99)
df.loc[df['value'] > Q99, 'value'] = Q99
重复值处理
- 完全重复:保留第一条,删除其他
- 部分重复:根据业务逻辑判断
- 如:3秒内的重复点击视为误触,保留一次
- 如:同一用户同一天的多次购买,全部保留
# 时间窗口去重
df = df.sort_values(['user_id', 'timestamp'])
df['time_diff'] = df.groupby('user_id')['timestamp'].diff()
df = df[(df['time_diff'].isna()) | (df['time_diff'] > 3)]
格式统一
- 时间格式:统一转换为标准格式
- 数值精度:统一小数位数
- 枚举值:统一大小写、编码
- 单位换算:统一度量单位
# 时间格式统一
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
# 枚举值统一
df['gender'] = df['gender'].str.lower().map({
'male': 'M', 'female': 'F', 'm': 'M', 'f': 'F'
})
实际案例:
在Steam评论分析项目中,遇到以下数据质量问题:
问题1:约5%的评论缺少游戏时长数据
- 处理:这些用户可能是退款或未实际游玩,标记为"未游玩"类别单独分析
问题2:部分评论时长异常(如9999小时)
- 处理:验证后发现是挂机用户,保留但在分析时单独标注
问题3:爬取的评论有HTML标签残留
- 处理:使用正则表达式清洗,去除所有HTML标签
数据质量处理原则
- 记录所有处理操作:方便回溯和验证
- 保留原始数据:处理后的数据另存,不覆盖原始数据
- 业务优先:技术处理要符合业务逻辑
- 透明化:在分析报告中说明数据质量情况和处理方式
- 持续优化:推动数据埋点和采集的优化
▶ 如何评估一个数据分析项目的成功?
评估数据分析项目的成功需要从多个维度考量,不能仅看分析本身:
业务价值维度:
是否解决了业务问题
- 项目最初的问题是否得到回答
- 提供的洞察是否具有可操作性
是否产生了商业影响
- 可量化的业务指标提升(如转化率+10%、GMV+100万)
- 成本节约或效率提升
- ROI计算:收益/投入
是否影响了决策
- 分析结果是否被采纳
- 是否推动了产品/运营策略的调整
分析质量维度:
数据可靠性
- 数据来源准确、完整
- 数据处理过程科学、规范
- 结论有充分的数据支撑
方法科学性
- 使用合适的分析模型和方法
- 统计检验严谨(如AB测试的显著性)
- 考虑了混淆因素和偏差
洞察深度
- 不止于描述现象,深入挖掘原因
- 提供了有价值的发现
- 洞察具有启发性
交付质量维度:
沟通效果
- 报告清晰易懂,非技术人员也能理解
- 可视化有效传达了关键信息
- 建议具体可行
及时性
- 按时完成,满足业务需求时间点
- 关键决策节点前提供分析支持
实际案例:
AB测试项目的成功评估:
- ✅ 业务价值:点击率提升11.52%,预计月GMV增长约50万
- ✅ 分析质量:12万样本量,统计显著性p<0.05,结论可信
- ✅ 交付质量:2周完成分析,推动产品全量上线新方案
- ✅ 后续影响:建立了AB测试流程规范,后续项目可复用
▶ 你在项目中用过哪些可视化工具?各有什么优缺点?
在数据分析项目中,我使用过多种可视化工具,根据不同场景选择:
Excel图表:
优点:
- 上手快,学习成本低
- 与数据处理无缝集成
- 适合快速探索和简单报表
缺点:
- 交互性弱,静态图表为主
- 复杂图表制作困难
- 大数据量处理性能差
使用场景:日常数据探索、简单的业务报表
Tableau:
优点:
- 可视化效果优秀,专业美观
- 拖拽式操作,无需编程
- 强大的交互功能和Dashboard
- 支持多数据源连接
缺点:
- 商业软件,成本较高
- 复杂定制需要学习
- 性能依赖数据量
使用场景:高管看板、业务分析报告、定期监控报表
ECharts:
优点:
- 开源免费,图表类型丰富
- 交互性强,动画效果好
- 可以嵌入网页,适合展示
- 高度可定制
缺点:
- 需要编写代码(JavaScript/JSON)
- 学习曲线较陡
- 需要前端开发知识
使用场景:Web展示、项目文档、技术报告
Python可视化(Matplotlib/Seaborn):
优点:
- 与数据分析流程无缝集成
- 高度灵活,可定制
- 适合科学计算和统计分析
缺点:
- 代码量大,需要编程能力
- 默认样式不够美观(需要调整)
- 交互性弱(静态图为主)
使用场景:数据探索、统计分析、学术研究
选择建议:
快速探索阶段
使用Excel或Python:
- Excel:小数据量,快速查看
- Python:大数据量,复杂分析
业务报告阶段
使用Tableau或PowerBI:
- 需要定期更新:Tableau Dashboard
- 需要美观专业:Tableau
- 需要交互探索:Tableau
技术文档/Web展示
使用ECharts或Plotly:
- 嵌入网页:ECharts
- 交互探索:Plotly
- 移动端展示:ECharts
在我的项目中:
- Steam评论分析:使用ECharts制作词云、情感分布图,嵌入HTML报告
- AB测试项目:使用Tableau制作监控Dashboard,实时追踪指标
- 岗位分析:使用Python+Matplotlib进行探索,ECharts制作最终展示图表
▶ 如何从零搭建一个用户画像系统?
用户画像是精准营销和个性化推荐的基础,我在电商用户分析项目中实践了完整的构建流程。
场景:为电商平台搭建用户画像系统,支持精准营销和个性化推荐。
完整构建流程:
1. 需求分析
明确画像维度:
- 基础属性:年龄、性别、地域、职业
- 行为属性:浏览频次、购买频次、访问时段
- 偏好属性:品类偏好、价格敏感度、品牌偏好
- 消费属性:客单价、生命周期价值、RFM
- 社交属性:分享行为、评论行为、社交影响力
2. 标签体系设计
# 三级标签体系
tags_hierarchy = {
'用户价值': {
'高价值用户': ['VIP用户', '超级会员', '企业客户'],
'中价值用户': ['普通会员', '活跃用户'],
'低价值用户': ['新用户', '沉睡用户']
},
'消费特征': {
'高频购买': ['每周购买', '月均3单以上'],
'价格敏感': ['爱用优惠券', '专买促销品']
}
}
def calculate_user_tags(user_data):
"""计算用户标签"""
tags = []
# RFM评分
rfm_score = (user_data['recency_score'] +
user_data['frequency_score'] +
user_data['monetary_score'])
if rfm_score >= 12:
tags.append('高价值用户')
elif rfm_score >= 8:
tags.append('中价值用户')
else:
tags.append('低价值用户')
# 品类偏好
if user_data['electronics_ratio'] > 0.5:
tags.append('电子产品爱好者')
# 价格敏感度
if user_data['avg_discount_usage'] > 0.7:
tags.append('价格敏感')
return tags
3. 数据ETL流程
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta
def extract_user_data(**context):
"""提取用户数据"""
date = context['ds']
# 从多个数据源提取
basic_info = extract_from_crm(date)
behavior_log = extract_from_clickhouse(date)
order_data = extract_from_mysql(date)
return {
'basic': basic_info,
'behavior': behavior_log,
'orders': order_data
}
def transform_user_profile(**context):
"""转换为用户画像"""
data = context['task_instance'].xcom_pull(task_ids='extract')
profiles = []
for user_id in data['basic']['user_id'].unique():
user_behaviors = data['behavior'][
data['behavior']['user_id'] == user_id
]
user_orders = data['orders'][
data['orders']['user_id'] == user_id
]
profile = {
'user_id': user_id,
# 行为特征
'pv_7d': len(user_behaviors),
'active_days_30d': user_behaviors['date'].nunique(),
# 交易特征
'order_count_30d': len(user_orders),
'total_amount_30d': user_orders['amount'].sum(),
# 标签
'tags': calculate_user_tags(...)
}
profiles.append(profile)
return profiles
def load_to_database(**context):
"""加载到数据库"""
profiles = context['task_instance'].xcom_pull(task_ids='transform')
# 存储到MySQL
pd.DataFrame(profiles).to_sql(
'user_profiles',
mysql_engine,
if_exists='replace'
)
# 同步到Redis(快速查询)
for profile in profiles:
redis_client.hset(
f"user:{profile['user_id']}",
mapping=profile
)
# Airflow DAG
dag = DAG(
'user_profile_etl',
default_args={'owner': 'data_team'},
schedule_interval='@daily', # 每天执行
start_date=datetime(2024, 1, 1)
)
extract_task = PythonOperator(
task_id='extract',
python_callable=extract_user_data,
dag=dag
)
transform_task = PythonOperator(
task_id='transform',
python_callable=transform_user_profile,
dag=dag
)
load_task = PythonOperator(
task_id='load',
python_callable=load_to_database,
dag=dag
)
extract_task >> transform_task >> load_task
4. 应用场景
场景1:精准营销
# 筛选目标用户发送优惠券
target_users = session.query(UserProfile).filter(
UserProfile.tags.contains('高价值用户'),
UserProfile.tags.contains('电子产品爱好者'),
UserProfile.recency > 30 # 30天未购买
).all()
for user in target_users:
send_coupon(user.user_id, category='electronics', discount=0.15)
场景2:个性化推荐
def recommend_for_user(user_id):
"""基于画像推荐商品"""
profile = get_user_profile(user_id)
# 根据偏好推荐
if '电子产品爱好者' in profile.tags:
products = get_new_electronics()
# 根据价格敏感度
if '价格敏感' in profile.tags:
products = filter_by_discount(products)
return products
项目成果:
- 构建50+用户标签
- 精准营销转化率提升35%
- 推荐点击率提升28%
- 每日自动更新画像
▶ 如何设计一次完整的AB测试?
AB测试是验证产品优化效果的科学方法,以商品详情页优化为例说明完整流程。
测试目标:验证新版详情页能否提升加购转化率。
关键步骤:
1. 假设与指标
- 假设:增加用户评价和视频介绍,可提升转化率5%以上
- 核心指标:加购转化率
- 次要指标:停留时长、下单率
- 防护指标:跳出率
2. 样本量计算
def calculate_sample_size(baseline=0.15, mde=0.05, alpha=0.05, power=0.8):
"""
计算所需样本量
baseline: 基线转化率15%
mde: 最小可检测效应5%
alpha: 显著性水平
power: 统计功效
"""
import scipy.stats as stats
import numpy as np
effect_size = mde / np.sqrt(baseline * (1 - baseline))
z_alpha = stats.norm.ppf(1 - alpha/2)
z_beta = stats.norm.ppf(power)
n = ((z_alpha + z_beta) / effect_size) ** 2
return int(np.ceil(n))
n_per_group = calculate_sample_size()
print(f"每组需要: {n_per_group}个样本")
# 输出: 每组需要: 6200个样本
3. 流量分配
import hashlib
def assign_group(user_id, experiment_id):
"""确定性分组"""
hash_value = int(hashlib.md5(
f"{user_id}_{experiment_id}".encode()
).hexdigest(), 16)
return 'treatment' if hash_value % 2 == 0 else 'control'
4. 埋点实施
// 前端埋点
const group = getExperimentGroup(userId, 'detail_page_v2');
// 页面加载
track('page_view', {
experiment_id: 'detail_page_v2',
group: group,
version: group === 'control' ? 'v1' : 'v2'
});
// 加购行为
document.querySelector('.add-cart').onclick = () => {
track('add_to_cart', {
experiment_id: 'detail_page_v2',
group: group
});
};
5. 结果分析
def analyze_ab_test(experiment_id):
"""分析AB测试结果"""
# 提取数据
df = get_experiment_data(experiment_id)
control = df[df['group'] == 'control']['converted']
treatment = df[df['group'] == 'treatment']['converted']
# t检验
from scipy import stats
t_stat, p_value = stats.ttest_ind(control, treatment)
# 计算提升
control_rate = control.mean()
treatment_rate = treatment.mean()
lift = (treatment_rate - control_rate) / control_rate
print(f"""
对照组转化率: {control_rate:.2%}
实验组转化率: {treatment_rate:.2%}
相对提升: {lift:.2%}
P值: {p_value:.4f}
是否显著: {'是' if p_value < 0.05 else '否'}
""")
# 决策
if p_value < 0.05 and lift > 0:
return "建议全量上线"
else:
return "建议继续实验或放弃"
实验结果:
- 新版转化率提升6.8%(p=0.003)
- 停留时长增加15秒
- 跳出率无恶化
- 决策:全量上线
常见陷阱
- 过早停止:未达到预定样本量就结束
- 多重比较:同时测试多个指标不校正α
- 新奇效应:实验时间太短
- Simpson悖论:忽略分层因素
▶ 遇到数据倾斜问题如何解决?
数据倾斜导致Spark任务中个别Task运行时间过长。
问题表现:
- 99%的Task在30分钟完成,1%需要4.5小时
- 某Executor内存达95%,其他只有20%
- 频繁OOM错误
解决方案:
方法1:加盐法
from pyspark.sql.functions import rand, concat, lit
# 原代码(倾斜)
result = orders.groupBy('merchant_id').agg(
F.sum('amount').alias('total')
)
# 加盐打散
salt_num = 100
orders_salted = orders.withColumn(
'merchant_id_salted',
concat(col('merchant_id'), lit('_'), (rand() * salt_num).cast('int'))
)
# 两阶段聚合
partial = orders_salted.groupBy('merchant_id_salted').agg(
F.sum('amount').alias('partial_sum')
)
final = partial.withColumn(
'merchant_id',
F.split(col('merchant_id_salted'), '_')[0]
).groupBy('merchant_id').agg(
F.sum('partial_sum').alias('total')
)
方法2:分而治之
# 识别倾斜Key
skewed_threshold = 10000
counts = orders.groupBy('merchant_id').count()
skewed_ids = counts.filter(col('count') > skewed_threshold)
# 分离处理
normal_orders = orders.filter(~col('merchant_id').isin(skewed_ids))
skewed_orders = orders.filter(col('merchant_id').isin(skewed_ids))
# 正常数据正常处理
normal_result = normal_orders.groupBy('merchant_id').agg(...)
# 倾斜数据加盐处理
skewed_result = process_with_salt(skewed_orders)
# 合并
final = normal_result.union(skewed_result)
方法3:广播Join
# 小表Join大表
from pyspark.sql.functions import broadcast
# 原代码
result = large_orders.join(merchants, 'merchant_id')
# 广播小表
result = large_orders.join(
broadcast(merchants), # merchants表小于2GB
'merchant_id'
)
效果对比:
| 方法 | 执行时间 | 内存峰值 |
|---|---|---|
| 原始 | 5小时 | 95% |
| 加盐法 | 45分钟 | 65% |
| 分而治之 | 35分钟 | 60% |
| 广播Join | 20分钟 | 70% |
Python数据结构
▶ 列表(List)的常用操作有哪些?
列表是Python中最常用的数据结构之一,可变、有序、允许重复。
创建列表:
# 空列表
lst = []
lst = list()
# 带初始值
lst = [1, 2, 3, 4, 5]
lst = list(range(1, 6)) # [1, 2, 3, 4, 5]
# 列表推导式
lst = [x**2 for x in range(5)] # [0, 1, 4, 9, 16]
访问元素:
索引访问
lst = [10, 20, 30, 40, 50]
# 正向索引(从0开始)
lst[0] # 10
lst[2] # 30
# 负向索引(从-1开始)
lst[-1] # 50(最后一个)
lst[-2] # 40(倒数第二个)
# 越界会报错
lst[10] # IndexError
切片操作
lst = [10, 20, 30, 40, 50]
# 基本切片 [start:stop:step]
lst[1:4] # [20, 30, 40](包含start,不包含stop)
lst[:3] # [10, 20, 30](从头开始)
lst[2:] # [30, 40, 50](到尾结束)
lst[:] # [10, 20, 30, 40, 50](复制列表)
# 步长
lst[::2] # [10, 30, 50](每隔一个)
lst[1::2] # [20, 40]
# 反转
lst[::-1] # [50, 40, 30, 20, 10]
# 负索引切片
lst[-3:-1] # [30, 40]
遍历列表
lst = ['a', 'b', 'c']
# 遍历元素
for item in lst:
print(item)
# 遍历索引和元素
for i, item in enumerate(lst):
print(f"{i}: {item}")
# 遍历多个列表
lst2 = [1, 2, 3]
for x, y in zip(lst, lst2):
print(x, y)
修改列表:
lst = [1, 2, 3]
# 添加元素
lst.append(4) # [1, 2, 3, 4](末尾添加)
lst.insert(1, 99) # [1, 99, 2, 3, 4](指定位置插入)
lst.extend([5, 6]) # [1, 99, 2, 3, 4, 5, 6](批量添加)
lst += [7, 8] # 同extend
# 删除元素
lst.remove(99) # [1, 2, 3, 4, 5, 6, 7, 8](删除首个值为99的元素)
item = lst.pop() # 删除并返回最后一个元素
item = lst.pop(0) # 删除并返回索引0的元素
del lst[0] # 删除索引0的元素
lst.clear() # 清空列表
# 修改元素
lst[0] = 100 # 单个修改
lst[1:3] = [200, 300] # 批量修改
列表常用方法:
lst = [3, 1, 4, 1, 5, 9, 2, 6]
# 查找
lst.index(4) # 2(返回首个值为4的索引)
lst.count(1) # 2(统计1出现的次数)
4 in lst # True(判断是否存在)
# 排序
lst.sort() # [1, 1, 2, 3, 4, 5, 6, 9](原地排序)
lst.sort(reverse=True) # 降序
sorted_lst = sorted(lst) # 返回新列表,不改变原列表
# 反转
lst.reverse() # 原地反转
reversed_lst = lst[::-1] # 返回新列表
# 其他
len(lst) # 长度
max(lst) # 最大值
min(lst) # 最小值
sum(lst) # 求和
列表推导式(高级):
# 基本形式
[表达式 for 变量 in 可迭代对象]
# 示例
[x**2 for x in range(5)] # [0, 1, 4, 9, 16]
# 带条件
[x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]
# 多重循环
[(x, y) for x in range(3) for y in range(2)]
# [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
# 嵌套列表推导式
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
[item for row in matrix for item in row] # [1, 2, 3, 4, 5, 6, 7, 8, 9]
列表使用技巧
列表复制:
# 浅拷贝 new_lst = lst.copy() new_lst = lst[:] new_lst = list(lst) # 深拷贝(嵌套列表) import copy new_lst = copy.deepcopy(lst)列表去重:
lst = [1, 2, 2, 3, 3, 3] unique_lst = list(set(lst)) # 无序 unique_lst = list(dict.fromkeys(lst)) # 保持顺序列表拼接:
lst1 + lst2 # 创建新列表 lst1.extend(lst2) # 修改lst1
▶ 字典(Dict)的常用操作有哪些?
字典是键值对的集合,可变、无序(Python 3.7+保持插入顺序)、键唯一。
创建字典:
# 空字典
d = {}
d = dict()
# 带初始值
d = {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
# 使用dict()函数
d = dict(name='Alice', age=25)
d = dict([('name', 'Alice'), ('age', 25)])
# 字典推导式
d = {x: x**2 for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
访问与修改:
基本操作
d = {'name': 'Alice', 'age': 25}
# 访问
d['name'] # 'Alice'
d['city'] # KeyError(键不存在会报错)
# 修改
d['age'] = 26 # 修改已有键
d['city'] = 'Shanghai' # 添加新键
# 删除
del d['age'] # 删除键值对
value = d.pop('name') # 删除并返回值
d.pop('city', None) # 键不存在时返回默认值
d.clear() # 清空字典
安全访问
d = {'name': 'Alice'}
# get方法(推荐)
d.get('name') # 'Alice'
d.get('age') # None(键不存在返回None)
d.get('age', 0) # 0(自定义默认值)
# setdefault
d.setdefault('age', 25) # 键不存在时设置并返回
d.setdefault('name', 'Bob') # 键存在时返回原值
# defaultdict(需要import)
from collections import defaultdict
d = defaultdict(int) # 默认值为0
d['count'] += 1 # 不需要初始化
批量更新
d1 = {'a': 1, 'b': 2}
d2 = {'b': 3, 'c': 4}
# update方法
d1.update(d2) # {'a': 1, 'b': 3, 'c': 4}(d2覆盖d1)
# 字典解包(Python 3.9+)
d3 = {**d1, **d2}
# 合并运算符(Python 3.9+)
d3 = d1 | d2
字典常用方法:
d = {'name': 'Alice', 'age': 25, 'city': 'Beijing'}
# 获取所有键
keys = d.keys() # dict_keys(['name', 'age', 'city'])
keys_list = list(d.keys())
# 获取所有值
values = d.values() # dict_values(['Alice', 25, 'Beijing'])
# 获取所有键值对
items = d.items() # dict_items([('name', 'Alice'), ...])
# 遍历
for key in d:
print(key, d[key])
for key, value in d.items():
print(key, value)
# 判断键是否存在
'name' in d # True
'email' in d # False
# 复制
d2 = d.copy() # 浅拷贝
字典推导式:
# 基本形式
{key_expr: value_expr for 变量 in 可迭代对象}
# 示例
{x: x**2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# 带条件
{x: x**2 for x in range(10) if x % 2 == 0}
# 交换键值
d = {'a': 1, 'b': 2, 'c': 3}
{v: k for k, v in d.items()} # {1: 'a', 2: 'b', 3: 'c'}
# 从两个列表创建字典
keys = ['name', 'age', 'city']
values = ['Alice', 25, 'Beijing']
d = {k: v for k, v in zip(keys, values)}
常见应用场景:
# 1. 计数
from collections import Counter
lst = ['a', 'b', 'a', 'c', 'b', 'a']
count = Counter(lst) # Counter({'a': 3, 'b': 2, 'c': 1})
# 手动实现
count = {}
for item in lst:
count[item] = count.get(item, 0) + 1
# 2. 分组
from collections import defaultdict
students = [
{'name': 'Alice', 'class': 'A'},
{'name': 'Bob', 'class': 'B'},
{'name': 'Charlie', 'class': 'A'}
]
groups = defaultdict(list)
for student in students:
groups[student['class']].append(student['name'])
# {'A': ['Alice', 'Charlie'], 'B': ['Bob']}
# 3. 缓存/记忆化
cache = {}
def fib(n):
if n in cache:
return cache[n]
if n <= 1:
return n
cache[n] = fib(n-1) + fib(n-2)
return cache[n]
字典使用注意事项
键的要求:
- 必须是不可变类型(str, int, tuple等)
- list, dict不能作为键
d = {[1, 2]: 'value'} # TypeError d = {(1, 2): 'value'} # OK字典是无序的(Python 3.6-):
- Python 3.7+ 保持插入顺序
- 但不应依赖顺序,需要顺序用OrderedDict
遍历时不要修改:
# 错误 for key in d: if some_condition: del d[key] # RuntimeError # 正确 keys_to_delete = [k for k in d if some_condition] for key in keys_to_delete: del d[key]
▶ 元组(Tuple)和集合(Set)有什么特点?
元组和集合是Python中两种重要的数据结构,各有不同的特点和应用场景。
特点对比:
| 特性 | 元组(Tuple) | 集合(Set) |
|---|---|---|
| 可变性 | 不可变 | 可变 |
| 有序性 | 有序(保持插入顺序) | 无序(不保证顺序) |
| 重复元素 | 允许重复 | 元素唯一(自动去重) |
| 索引访问 | 支持(t[0]) | 不支持 |
| 可哈希 | 可作为字典键 | 不可作为字典键 |
| 性能 | 访问快 | 查找快(O(1)) |
| 内存 | 占用少 | 占用多(哈希表) |
| 语法 | (1, 2, 3) | {1, 2, 3} |
元组(Tuple)详解:
元组基础
# 创建元组
t = () # 空元组
t = (1,) # 单元素元组(注意逗号)
t = (1, 2, 3)
t = 1, 2, 3 # 可以省略括号
t = tuple([1, 2, 3]) # 从列表创建
# 访问
t[0] # 第一个元素
t[-1] # 最后一个元素
t[1:3] # 切片,返回新元组
# 不可修改
t[0] = 100 # TypeError(不可修改)
元组操作
t = (1, 2, 3, 2, 1)
# 基本操作
len(t) # 5
2 in t # True
t.count(2) # 2(统计出现次数)
t.index(3) # 2(返回首次出现的索引)
# 拼接
t1 = (1, 2)
t2 = (3, 4)
t3 = t1 + t2 # (1, 2, 3, 4)
# 重复
t = (1, 2) * 3 # (1, 2, 1, 2, 1, 2)
# 解包
a, b, c = (1, 2, 3) # a=1, b=2, c=3
a, *b, c = (1, 2, 3, 4, 5) # a=1, b=[2,3,4], c=5
# 交换变量
a, b = b, a
应用场景
- 函数返回多个值:
def get_stats(data):
return len(data), sum(data), sum(data)/len(data)
count, total, avg = get_stats([1, 2, 3, 4, 5])
- 作为字典的键:
# 列表不能作为键,元组可以
d = {(1, 2): 'point1', (3, 4): 'point2'}
- 保护数据不被修改:
config = ('localhost', 8080, 'utf-8') # 配置不可变
- namedtuple(命名元组):
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(p.x, p.y) # 1 2(可以用属性访问)
集合(Set)详解:
集合基础
# 创建集合
s = set() # 空集合(不能用{},那是空字典)
s = {1, 2, 3}
s = set([1, 2, 3, 2, 1]) # {1, 2, 3}(自动去重)
# 添加元素
s.add(4) # {1, 2, 3, 4}
s.update([5, 6]) # {1, 2, 3, 4, 5, 6}
s.update({7, 8}, [9]) # 可以多个可迭代对象
# 删除元素
s.remove(1) # 删除元素(不存在会报错)
s.discard(1) # 删除元素(不存在不报错)
elem = s.pop() # 随机删除并返回一个元素
s.clear() # 清空集合
集合运算
s1 = {1, 2, 3, 4}
s2 = {3, 4, 5, 6}
# 并集
s1 | s2 # {1, 2, 3, 4, 5, 6}
s1.union(s2)
# 交集
s1 & s2 # {3, 4}
s1.intersection(s2)
# 差集
s1 - s2 # {1, 2}(在s1但不在s2)
s1.difference(s2)
# 对称差集
s1 ^ s2 # {1, 2, 5, 6}(在s1或s2但不同时在)
s1.symmetric_difference(s2)
# 子集和超集
{1, 2} <= {1, 2, 3} # True(子集)
{1, 2, 3} >= {1, 2} # True(超集)
应用场景
- 去重:
lst = [1, 2, 2, 3, 3, 3]
unique_lst = list(set(lst))
- 成员检测(比列表快):
# O(1) 时间复杂度
if item in large_set: # 很快
pass
if item in large_list: # 较慢
pass
- 集合运算:
# 找出两组用户的交集、差集
active_users = {1, 2, 3, 4, 5}
paid_users = {3, 4, 5, 6, 7}
# 活跃且付费的用户
active_and_paid = active_users & paid_users # {3, 4, 5}
# 活跃但未付费的用户
active_not_paid = active_users - paid_users # {1, 2}
- frozenset(不可变集合):
fs = frozenset([1, 2, 3])
# 可以作为字典的键或集合的元素
d = {fs: 'value'}
元组vs列表,集合vs列表对比:
| 特性 | 列表 | 元组 | 集合 |
|---|---|---|---|
| 可变性 | 可变 | 不可变 | 可变 |
| 有序性 | 有序 | 有序 | 无序 |
| 重复元素 | 允许 | 允许 | 不允许 |
| 索引访问 | 支持 | 支持 | 不支持 |
| 性能 | 一般 | 稍快 | 查找快 |
| 内存 | 较大 | 较小 | 中等 |
| 作为字典键 | 不可以 | 可以 | 不可以 |
Python核心特性
▶ 什么是Lambda表达式?怎么用?有什么应用场景?
Lambda是Python中的匿名函数,用于创建简单的、一次性使用的函数。
基本语法:
lambda 参数: 表达式
特点:
- 只能包含一个表达式(不能有语句)
- 自动返回表达式的值
- 通常用于简单的功能
基础用法:
基本示例
# lambda函数
square = lambda x: x**2
print(square(5)) # 25
# 等价的普通函数
def square(x):
return x**2
# 多个参数
add = lambda x, y: x + y
print(add(3, 4)) # 7
# 默认参数
greet = lambda name="World": f"Hello, {name}!"
print(greet()) # Hello, World!
print(greet("Alice")) # Hello, Alice!
# 条件表达式
max_val = lambda a, b: a if a > b else b
print(max_val(3, 5)) # 5
与普通函数对比
# 普通函数 - 适合复杂逻辑
def process_data(data):
result = []
for item in data:
if item > 0:
result.append(item * 2)
return result
# Lambda - 适合简单操作
double = lambda x: x * 2
is_positive = lambda x: x > 0
何时用Lambda:
- 函数逻辑简单(一行)
- 函数只用一次
- 作为参数传递
何时用普通函数:
- 逻辑复杂(多行)
- 需要重复使用
- 需要文档字符串
作为参数传递
# 排序
students = [
{'name': 'Alice', 'age': 25, 'score': 85},
{'name': 'Bob', 'age': 22, 'score': 92},
{'name': 'Charlie', 'age': 23, 'score': 78}
]
# 按年龄排序
sorted_by_age = sorted(students, key=lambda x: x['age'])
# 按成绩降序
sorted_by_score = sorted(students,
key=lambda x: x['score'],
reverse=True)
# 按多个条件(年龄升序,成绩降序)
sorted_multi = sorted(students,
key=lambda x: (x['age'], -x['score']))
常见应用场景:
1. 配合map()使用:
# 对列表每个元素应用函数
numbers = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, numbers))
# [1, 4, 9, 16, 25]
# 多个列表
a = [1, 2, 3]
b = [4, 5, 6]
result = list(map(lambda x, y: x + y, a, b))
# [5, 7, 9]
2. 配合filter()使用:
# 筛选符合条件的元素
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even = list(filter(lambda x: x % 2 == 0, numbers))
# [2, 4, 6, 8, 10]
# 筛选数据
students = [
{'name': 'Alice', 'score': 85},
{'name': 'Bob', 'score': 92},
{'name': 'Charlie', 'score': 78}
]
passed = list(filter(lambda x: x['score'] >= 80, students))
3. 配合sorted()使用:
# 自定义排序规则
words = ['apple', 'Banana', 'cherry', 'Date']
# 按长度排序
sorted(words, key=lambda x: len(x))
# 忽略大小写排序
sorted(words, key=lambda x: x.lower())
# 复杂对象排序
data = [(1, 'b'), (2, 'a'), (1, 'a')]
sorted(data, key=lambda x: (x[0], x[1]))
4. 配合pandas使用:
import pandas as pd
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'age': [25, 22, 23],
'salary': [5000, 6000, 5500]
})
# apply + lambda
df['age_group'] = df['age'].apply(lambda x: '20-25' if x <= 25 else '25+')
# 多列计算
df['bonus'] = df.apply(lambda row: row['salary'] * 0.1 if row['age'] > 23 else 0, axis=1)
# map替换值
df['name'] = df['name'].map(lambda x: x.upper())
5. 条件判断:
# 条件表达式
status = lambda score: 'Pass' if score >= 60 else 'Fail'
print(status(75)) # Pass
# 多条件
grade = lambda score: 'A' if score >= 90 else 'B' if score >= 80 else 'C' if score >= 70 else 'D'
print(grade(85)) # B
Lambda最佳实践
推荐用法:
# 1. 作为参数,简单转换
sorted(data, key=lambda x: x['value'])
list(map(lambda x: x*2, numbers))
# 2. 简单的一次性函数
df['category'] = df['age'].apply(lambda x: 'adult' if x >= 18 else 'minor')
不推荐用法:
# 1. 赋值给变量(不如用def)
square = lambda x: x**2 # 不好
def square(x): return x**2 # 更好
# 2. 复杂逻辑
# 不好
complex = lambda x: x**2 if x > 0 else -x**2 if x < 0 else 0
# 更好
def complex(x):
if x > 0:
return x**2
elif x < 0:
return -x**2
else:
return 0
# 3. 多行逻辑(lambda不支持)
记住:Lambda的目的是简洁,如果不够简洁,就用普通函数。
▶ 字符串的常用操作有哪些?
字符串是Python中最常用的数据类型之一,不可变、有序。
字符串创建:
# 单引号、双引号、三引号
s = 'Hello'
s = "Hello"
s = '''多行
字符串'''
# 原始字符串(转义符不生效)
s = r'C:\new\test' # 不会解析\n为换行
# 格式化字符串
name = 'Alice'
age = 25
s = f'My name is {name}, I am {age} years old'
s = 'My name is {}, I am {} years old'.format(name, age)
s = 'My name is %s, I am %d years old' % (name, age)
字符串操作:
查找与判断
s = 'Hello, World!'
# 查找
s.find('World') # 7(返回首次出现的索引,不存在返回-1)
s.index('World') # 7(不存在会报错)
s.rfind('o') # 8(从右开始查找)
s.count('l') # 3(统计出现次数)
# 判断
s.startswith('Hello') # True
s.endswith('!') # True
'World' in s # True
# 类型判断
'123'.isdigit() # True(全是数字)
'abc'.isalpha() # True(全是字母)
'abc123'.isalnum() # True(字母或数字)
' '.isspace() # True(全是空白字符)
'Hello'.isupper() # False
'hello'.islower() # True
切片与拼接
s = 'Hello, World!'
# 切片
s[0:5] # 'Hello'
s[:5] # 'Hello'
s[7:] # 'World!'
s[::2] # 'Hlo ol!'(步长为2)
s[::-1] # '!dlroW ,olleH'(反转)
# 拼接
'Hello' + ' ' + 'World' # 'Hello World'
' '.join(['Hello', 'World']) # 'Hello World'
'Hello' * 3 # 'HelloHelloHello'
# 效率对比
# 不推荐(多次拼接)
s = ''
for word in words:
s += word # 每次创建新字符串
# 推荐
s = ''.join(words) # 一次性拼接
大小写转换
s = 'Hello, World!'
s.upper() # 'HELLO, WORLD!'
s.lower() # 'hello, world!'
s.capitalize() # 'Hello, world!'(首字母大写)
s.title() # 'Hello, World!'(每个单词首字母大写)
s.swapcase() # 'hELLO, wORLD!'(大小写互换)
分割与连接
s = 'apple,banana,cherry'
# 分割
s.split(',') # ['apple', 'banana', 'cherry']
s.split(',', 1) # ['apple', 'banana,cherry'](最多分割1次)
# 按行分割
text = 'line1\nline2\nline3'
text.splitlines() # ['line1', 'line2', 'line3']
# 分割保留分隔符
s.partition(',') # ('apple', ',', 'banana,cherry')
s.rpartition(',') # ('apple,banana', ',', 'cherry')
# 连接
','.join(['a', 'b', 'c']) # 'a,b,c'
' - '.join(['2024', '10', '29']) # '2024 - 10 - 29'
字符串清理与替换:
s = ' Hello, World! '
# 去除空白
s.strip() # 'Hello, World!'(去除两端)
s.lstrip() # 'Hello, World! '(去除左边)
s.rstrip() # ' Hello, World!'(去除右边)
s.strip('!') # ' Hello, World '(去除指定字符)
# 替换
s = 'Hello, World!'
s.replace('World', 'Python') # 'Hello, Python!'
s.replace('l', 'L', 2) # 'HeLLo, World!'(最多替换2次)
# 正则替换
import re
s = 'abc123def456'
re.sub(r'\d+', 'X', s) # 'abcXdefX'(数字替换为X)
字符串对齐与填充:
s = 'Hello'
# 对齐
s.ljust(10) # 'Hello '(左对齐,宽度10)
s.rjust(10) # ' Hello'(右对齐)
s.center(10) # ' Hello '(居中)
s.center(10, '-') # '--Hello---'(用-填充)
# 补零
'42'.zfill(5) # '00042'
'-42'.zfill(5) # '-0042'
字符串编码:
# Unicode字符串 → 字节
s = '你好'
b = s.encode('utf-8') # b'\xe4\xbd\xa0\xe5\xa5\xbd'
b = s.encode('gbk')
# 字节 → Unicode字符串
s = b.decode('utf-8') # '你好'
实用示例:
# 1. 清洗数据
text = ' Hello, World! \n'
clean = ' '.join(text.split()) # 'Hello, World!'(去除多余空白)
# 2. 提取数字
s = 'Total: $1,234.56'
import re
numbers = re.findall(r'\d+', s) # ['1', '234', '56']
amount = float(s.replace('$', '').replace(',', '').split()[-1]) # 1234.56
# 3. 验证格式
email = 'user@example.com'
if '@' in email and '.' in email.split('@')[-1]:
print('Valid email')
# 4. 生成slug
title = 'Hello World: Python Programming!'
slug = '-'.join(title.lower().split()).strip('!:') # 'hello-world-python-programming'
# 5. 文本对齐(表格)
data = [['Name', 'Age', 'City'],
['Alice', '25', 'Beijing'],
['Bob', '22', 'Shanghai']]
for row in data:
print(' | '.join(item.ljust(10) for item in row))
字符串使用注意事项
字符串是不可变的:
s = 'Hello' s[0] = 'h' # TypeError(不能修改) s = 'h' + s[1:] # 创建新字符串大量拼接用join:
# 不好(慢) result = '' for i in range(10000): result += str(i) # 好(快) result = ''.join(str(i) for i in range(10000))注意编码:
# 读取文件指定编码 with open('file.txt', encoding='utf-8') as f: content = f.read()
▶ 什么是面向对象?类和实例的关系?
面向对象编程(OOP)是一种编程范式,将数据和操作数据的方法封装在一起,形成"对象"。
核心概念:
- 类(Class):对象的模板/蓝图,定义了对象的属性和方法
- 对象/实例(Object/Instance):类的具体实现,一个类可以创建多个对象
- 属性(Attribute):对象的数据
- 方法(Method):对象的行为/功能
类和实例的关系:
# 类是模板
class Dog:
pass
# 实例是根据模板创建的具体对象
dog1 = Dog() # 第一只狗
dog2 = Dog() # 第二只狗
# dog1和dog2是两个不同的对象
print(dog1 is dog2) # False
基本语法:
定义类
class Person:
# 类属性(所有实例共享)
species = 'Human'
# 构造方法(初始化)
def __init__(self, name, age):
# 实例属性(每个实例独有)
self.name = name
self.age = age
# 实例方法
def greet(self):
return f"Hello, I'm {self.name}"
# 类方法(操作类属性)
@classmethod
def get_species(cls):
return cls.species
# 静态方法(与类/实例无关的工具方法)
@staticmethod
def is_adult(age):
return age >= 18
实例化对象
# 创建实例
alice = Person('Alice', 25)
bob = Person('Bob', 17)
# 访问属性
print(alice.name) # Alice
print(alice.age) # 25
print(alice.species) # Human
# 调用方法
print(alice.greet()) # Hello, I'm Alice
# 类方法
print(Person.get_species()) # Human
# 静态方法
print(Person.is_adult(20)) # True
print(alice.is_adult(15)) # False(也可以通过实例调用)
属性和方法
class BankAccount:
def __init__(self, owner, balance=0):
self.owner = owner
self._balance = balance # _表示内部使用(约定)
# Getter方法
def get_balance(self):
return self._balance
# 业务方法
def deposit(self, amount):
if amount > 0:
self._balance += amount
return True
return False
def withdraw(self, amount):
if 0 < amount <= self._balance:
self._balance -= amount
return True
return False
# property装饰器(将方法变为属性)
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value >= 0:
self._balance = value
# 使用
account = BankAccount('Alice', 1000)
account.deposit(500)
print(account.balance) # 1500(通过property访问)
account.balance = 2000 # 通过setter修改
特殊方法(魔法方法)
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
# 字符串表示
def __str__(self):
return f'Vector({self.x}, {self.y})'
def __repr__(self):
return f'Vector({self.x}, {self.y})'
# 运算符重载
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __eq__(self, other):
return self.x == other.x and self.y == other.y
# 长度
def __len__(self):
return int((self.x**2 + self.y**2)**0.5)
# 使用
v1 = Vector(1, 2)
v2 = Vector(3, 4)
print(v1) # Vector(1, 2)
v3 = v1 + v2 # Vector(4, 6)
print(v1 == v2) # False
print(len(v1)) # 2
面向对象的三大特性:
1. 封装(Encapsulation):
class Student:
def __init__(self, name, score):
self.name = name
self.__score = score # __表示私有(name mangling)
def get_score(self):
return self.__score
def set_score(self, score):
if 0 <= score <= 100:
self.__score = score
else:
raise ValueError('Score must be 0-100')
s = Student('Alice', 85)
print(s.get_score()) # 85
# print(s.__score) # AttributeError(无法直接访问)
s.set_score(90) # OK
2. 继承(Inheritance):
# 父类
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
pass
# 子类继承父类
class Dog(Animal):
def speak(self):
return f'{self.name} says Woof!'
class Cat(Animal):
def speak(self):
return f'{self.name} says Meow!'
# 使用
dog = Dog('Buddy')
cat = Cat('Kitty')
print(dog.speak()) # Buddy says Woof!
print(cat.speak()) # Kitty says Meow!
# isinstance检查
print(isinstance(dog, Dog)) # True
print(isinstance(dog, Animal)) # True
3. 多态(Polymorphism):
# 不同对象调用相同方法,表现不同行为
def animal_speak(animal):
print(animal.speak())
dog = Dog('Buddy')
cat = Cat('Kitty')
animal_speak(dog) # Buddy says Woof!
animal_speak(cat) # Kitty says Meow!
实际应用示例:
# 数据分析中的应用
class DataProcessor:
def __init__(self, data):
self.data = data
self.cleaned_data = None
def load_data(self, filepath):
import pandas as pd
self.data = pd.read_csv(filepath)
return self
def clean_data(self):
# 数据清洗逻辑
self.cleaned_data = self.data.dropna()
return self
def analyze(self):
# 分析逻辑
return self.cleaned_data.describe()
def save_result(self, filepath):
self.cleaned_data.to_csv(filepath, index=False)
return self
# 链式调用
processor = DataProcessor(None)
result = (processor
.load_data('data.csv')
.clean_data()
.analyze())
面向对象使用建议
何时使用OOP:
- 需要封装数据和行为
- 代码需要复用(继承)
- 需要维护状态
- 建模真实世界的对象
何时不用OOP:
- 简单脚本
- 数据处理管道(可以用函数式编程)
- 性能敏感场景(OOP有开销)
Python的OOP特点:
- 一切皆对象(包括函数、类本身)
- 支持多继承
- 动态语言,运行时可以修改类
- Duck Typing:关注行为而非类型
Python实战场景
▶ 如何处理大文件读取避免内存溢出?
在数据分析中经常遇到几GB甚至更大的数据文件,一次性读取会导致内存不足。
场景:处理一个5GB的日志文件,提取包含"ERROR"的行。
分块读取文本文件
def process_large_file(filepath, chunk_size=1024*1024):
"""
分块读取大文件
chunk_size: 每次读取的字节数,默认1MB
"""
error_lines = []
with open(filepath, 'r', encoding='utf-8') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
# 处理每个chunk
lines = chunk.split('\n')
for line in lines:
if 'ERROR' in line:
error_lines.append(line)
return error_lines
# 使用示例
errors = process_large_file('large_log.txt')
生成器逐行读取
def read_large_file(filepath):
"""
使用生成器逐行读取,内存占用恒定
"""
with open(filepath, 'r', encoding='utf-8') as f:
for line in f: # 文件对象本身就是迭代器
yield line.strip()
def process_errors(filepath):
"""
处理错误日志
"""
error_count = 0
error_types = {}
for line in read_large_file(filepath):
if 'ERROR' in line:
error_count += 1
# 统计错误类型
error_type = line.split(':')[0] if ':' in line else 'Unknown'
error_types[error_type] = error_types.get(error_type, 0) + 1
return error_count, error_types
# 使用示例
count, types = process_errors('large_log.txt')
print(f"总错误数: {count}")
print(f"错误类型分布: {types}")
Pandas分块读取CSV
import pandas as pd
def process_large_csv(filepath, chunksize=10000):
"""
分块处理大型CSV文件
chunksize: 每块的行数
"""
# 初始化统计变量
total_revenue = 0
user_count = 0
# 分块读取
for chunk in pd.read_csv(filepath, chunksize=chunksize):
# 处理每个chunk
total_revenue += chunk['revenue'].sum()
user_count += chunk['user_id'].nunique()
# 可以在这里做筛选、聚合等操作
high_value = chunk[chunk['revenue'] > 1000]
# 保存高价值用户到新文件
high_value.to_csv('high_value_users.csv',
mode='a', # 追加模式
header=False,
index=False)
return total_revenue, user_count
# 使用示例
revenue, users = process_large_csv('large_data.csv')
print(f"总收入: {revenue}, 用户数: {users}")
使用itertools高效处理
from itertools import islice
def batch_process(filepath, batch_size=1000):
"""
批量处理文件,适合需要批量提交的场景
"""
with open(filepath, 'r') as f:
while True:
# 每次取batch_size行
batch = list(islice(f, batch_size))
if not batch:
break
# 批量处理这些行
process_batch(batch)
def process_batch(lines):
"""处理一批数据"""
# 批量插入数据库、批量API调用等
pass
# 使用示例
batch_process('large_log.txt', batch_size=1000)
最佳实践总结:
- 文本文件:使用文件迭代器逐行读取,内存占用最小
- CSV文件:使用pandas的chunksize参数分块处理
- 需要批处理:使用itertools.islice按批次读取
- 复杂数据处理:考虑使用Dask或Vaex等专门处理大数据的库
注意事项
- 分块大小要根据可用内存和处理速度权衡
- 跨chunk的数据(如被分割的行)需要特殊处理
- 使用生成器时注意只能迭代一次
▶ 如何处理API请求的重试和异常?
在数据采集场景中,API请求经常会因为网络波动、服务端限流等原因失败,需要实现重试机制。
场景:从第三方API获取用户数据,需要处理各种异常情况。
基础重试逻辑
import time
import requests
from requests.exceptions import RequestException
def fetch_data_with_retry(url, max_retries=3, delay=1):
"""
带重试的API请求
"""
for attempt in range(max_retries):
try:
response = requests.get(url, timeout=10)
response.raise_for_status() # 检查HTTP错误
return response.json()
except requests.exceptions.Timeout:
print(f"请求超时,第{attempt + 1}次重试...")
if attempt < max_retries - 1:
time.sleep(delay * (attempt + 1)) # 指数退避
else:
raise
except requests.exceptions.HTTPError as e:
# 处理HTTP错误
if response.status_code == 429: # 限流
print(f"触发限流,等待{delay * 2}秒...")
time.sleep(delay * 2)
elif response.status_code >= 500: # 服务器错误,重试
print(f"服务器错误,第{attempt + 1}次重试...")
time.sleep(delay)
else: # 客户端错误,不重试
raise
except RequestException as e:
print(f"请求异常: {e}")
if attempt < max_retries - 1:
time.sleep(delay)
else:
raise
return None
# 使用示例
data = fetch_data_with_retry('https://api.example.com/users')
装饰器实现重试
import functools
import time
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
"""
重试装饰器
max_attempts: 最大尝试次数
delay: 初始延迟时间
backoff: 退避系数
exceptions: 需要重试的异常类型
"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
current_delay = delay
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except exceptions as e:
attempts += 1
if attempts >= max_attempts:
print(f"达到最大重试次数({max_attempts}),放弃")
raise
print(f"第{attempts}次失败: {e},{current_delay}秒后重试...")
time.sleep(current_delay)
current_delay *= backoff # 指数退避
return wrapper
return decorator
# 使用装饰器
@retry(max_attempts=3, delay=1, backoff=2,
exceptions=(requests.exceptions.RequestException,))
def fetch_user_data(user_id):
"""获取用户数据"""
response = requests.get(f'https://api.example.com/users/{user_id}')
response.raise_for_status()
return response.json()
# 调用
user = fetch_user_data(123)
使用requests的Session和适配器
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
def create_retry_session(
retries=3,
backoff_factor=0.3,
status_forcelist=(500, 502, 504),
):
"""
创建带重试机制的session
"""
session = requests.Session()
retry_strategy = Retry(
total=retries,
read=retries,
connect=retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
allowed_methods=["HEAD", "GET", "OPTIONS", "POST"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("http://", adapter)
session.mount("https://", adapter)
return session
# 使用示例
session = create_retry_session()
def fetch_batch_data(user_ids):
"""批量获取用户数据"""
results = []
for user_id in user_ids:
try:
response = session.get(
f'https://api.example.com/users/{user_id}',
timeout=5
)
response.raise_for_status()
results.append(response.json())
except Exception as e:
print(f"获取用户{user_id}失败: {e}")
results.append(None)
return results
使用tenacity库(推荐)
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type
)
import requests
@retry(
stop=stop_after_attempt(3), # 最多3次
wait=wait_exponential(multiplier=1, min=2, max=10), # 指数退避
retry=retry_if_exception_type(requests.exceptions.RequestException)
)
def fetch_api_data(url):
"""使用tenacity的重试装饰器"""
response = requests.get(url, timeout=10)
response.raise_for_status()
return response.json()
# 更复杂的场景
from tenacity import retry_if_result
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=2, max=30),
retry=retry_if_result(lambda x: x is None) # 结果为None时重试
)
def fetch_data_until_success(url):
"""持续重试直到获取到有效数据"""
try:
response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
if data and 'result' in data:
return data['result']
except Exception as e:
print(f"请求失败: {e}")
return None # 返回None会触发重试
实战建议:
根据HTTP状态码决定是否重试:
- 4xx(客户端错误)通常不应重试
- 5xx(服务器错误)和超时应该重试
- 429(限流)需要增加等待时间
使用指数退避:避免瞬间大量请求冲击服务器
记录日志:重试过程和失败原因要记录
设置超时:避免请求无限等待
▶ 如何实现数据清洗的流水线?
数据分析项目中,数据清洗是必不可少的环节。如何优雅地实现一个可复用的清洗流程?
场景:清洗用户行为数据,包括去重、填充缺失值、格式转换、异常值处理等。
import pandas as pd
import numpy as np
from typing import Callable, List
class DataCleaningPipeline:
"""数据清洗流水线"""
def __init__(self, data: pd.DataFrame):
self.data = data
self.original_shape = data.shape
self.steps = []
def add_step(self, name: str, func: Callable, **kwargs):
"""添加清洗步骤"""
self.steps.append((name, func, kwargs))
return self
def execute(self):
"""执行所有步骤"""
print(f"原始数据: {self.original_shape}")
for step_name, func, kwargs in self.steps:
print(f"\n执行步骤: {step_name}")
before = len(self.data)
self.data = func(self.data, **kwargs)
after = len(self.data)
print(f" 处理前: {before}行, 处理后: {after}行")
if before != after:
print(f" 删除: {before - after}行")
print(f"\n最终数据: {self.data.shape}")
return self.data
def get_summary(self):
"""获取清洗摘要"""
return {
'original_shape': self.original_shape,
'final_shape': self.data.shape,
'steps': [step[0] for step in self.steps]
}
# 定义清洗函数
def remove_duplicates(df, subset=None):
"""去除重复值"""
return df.drop_duplicates(subset=subset, keep='first')
def fill_missing_values(df, strategy='mean', columns=None):
"""填充缺失值"""
df = df.copy()
if columns is None:
columns = df.select_dtypes(include=[np.number]).columns
for col in columns:
if strategy == 'mean':
df[col].fillna(df[col].mean(), inplace=True)
elif strategy == 'median':
df[col].fillna(df[col].median(), inplace=True)
elif strategy == 'mode':
df[col].fillna(df[col].mode()[0], inplace=True)
elif strategy == 'forward':
df[col].fillna(method='ffill', inplace=True)
return df
def remove_outliers(df, columns, method='iqr', threshold=1.5):
"""移除异常值"""
df = df.copy()
for col in columns:
if method == 'iqr':
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - threshold * IQR
upper = Q3 + threshold * IQR
df = df[(df[col] >= lower) & (df[col] <= upper)]
elif method == 'zscore':
z_scores = np.abs((df[col] - df[col].mean()) / df[col].std())
df = df[z_scores < threshold]
return df
def convert_types(df, conversions):
"""转换数据类型"""
df = df.copy()
for col, dtype in conversions.items():
df[col] = df[col].astype(dtype)
return df
def filter_rows(df, condition):
"""按条件筛选行"""
return df[condition(df)]
def transform_columns(df, transformations):
"""转换列数据"""
df = df.copy()
for col, func in transformations.items():
df[col] = df[col].apply(func)
return df
# 使用示例
# 创建示例数据
data = pd.DataFrame({
'user_id': [1, 2, 2, 3, 4, 5], # 有重复
'age': [25, 30, 30, None, 35, 200], # 有缺失和异常值
'income': [5000, 6000, 6000, 7000, None, 8000],
'registration_date': ['2023-01-01', '2023-01-02', '2023-01-02',
'2023-01-03', '2023-01-04', '2023-01-05']
})
# 构建清洗流水线
pipeline = DataCleaningPipeline(data)
pipeline.add_step(
'去除重复',
remove_duplicates,
subset=['user_id']
).add_step(
'填充缺失值',
fill_missing_values,
strategy='median',
columns=['age', 'income']
).add_step(
'移除异常值',
remove_outliers,
columns=['age'],
method='iqr'
).add_step(
'类型转换',
convert_types,
conversions={'registration_date': 'datetime64'}
).add_step(
'筛选有效用户',
filter_rows,
condition=lambda df: (df['age'] >= 18) & (df['age'] <= 100)
)
# 执行清洗
cleaned_data = pipeline.execute()
# 获取摘要
summary = pipeline.get_summary()
print(f"\n清洗摘要: {summary}")
输出示例:
原始数据: (6, 4)
执行步骤: 去除重复
处理前: 6行, 处理后: 5行
删除: 1行
执行步骤: 填充缺失值
处理前: 5行, 处理后: 5行
执行步骤: 移除异常值
处理前: 5行, 处理后: 4行
删除: 1行
执行步骤: 类型转换
处理前: 4行, 处理后: 4行
执行步骤: 筛选有效用户
处理前: 4行, 处理后: 4行
最终数据: (4, 4)
清洗摘要: {
'original_shape': (6, 4),
'final_shape': (4, 4),
'steps': ['去除重复', '填充缺失值', '移除异常值', '类型转换', '筛选有效用户']
}
优势:
- 可复用:定义一次,多处使用
- 可追踪:记录每一步的处理结果
- 易维护:添加、删除、修改步骤都很方便
- 链式调用:代码清晰易读
▶ 如何优化Python代码性能?
在处理大数据量时,Python代码的性能优化至关重要。
场景:优化一个用户行为统计函数,提升10倍以上性能。
使用列表推导式代替循环
import time
# 慢速版本:普通循环
def slow_filter(data, threshold):
result = []
for item in data:
if item > threshold:
result.append(item * 2)
return result
# 快速版本:列表推导式
def fast_filter(data, threshold):
return [item * 2 for item in data if item > threshold]
# 性能对比
data = list(range(1000000))
start = time.time()
result1 = slow_filter(data, 500000)
print(f"普通循环: {time.time() - start:.4f}秒")
start = time.time()
result2 = fast_filter(data, 500000)
print(f"列表推导式: {time.time() - start:.4f}秒")
# 典型输出:
# 普通循环: 0.1234秒
# 列表推导式: 0.0567秒 # 快2倍
使用NumPy向量化
import numpy as np
# 慢速版本:Python循环
def python_calculate(data):
result = []
for x in data:
result.append(x ** 2 + 2 * x + 1)
return result
# 快速版本:NumPy向量化
def numpy_calculate(data):
arr = np.array(data)
return arr ** 2 + 2 * arr + 1
# 性能对比
data = list(range(1000000))
start = time.time()
result1 = python_calculate(data)
print(f"Python循环: {time.time() - start:.4f}秒")
start = time.time()
result2 = numpy_calculate(data)
print(f"NumPy向量化: {time.time() - start:.4f}秒")
# 典型输出:
# Python循环: 0.2345秒
# NumPy向量化: 0.0123秒 # 快20倍!
# 实际案例:计算用户活跃度
def calculate_user_activity_slow(df):
"""慢速版本:遍历DataFrame"""
activity_scores = []
for idx, row in df.iterrows():
score = (row['login_days'] * 0.3 +
row['purchase_count'] * 0.5 +
row['review_count'] * 0.2)
activity_scores.append(score)
return activity_scores
def calculate_user_activity_fast(df):
"""快速版本:向量化操作"""
return (df['login_days'] * 0.3 +
df['purchase_count'] * 0.5 +
df['review_count'] * 0.2)
# 性能差异可达100倍以上!
Pandas避免apply,使用向量化
import pandas as pd
# 创建测试数据
df = pd.DataFrame({
'price': np.random.randint(10, 1000, 100000),
'quantity': np.random.randint(1, 100, 100000)
})
# 慢速版本:使用apply
def slow_calculate_total(df):
def calc(row):
return row['price'] * row['quantity'] * 1.1 # 加10%税
return df.apply(calc, axis=1)
# 快速版本:向量化
def fast_calculate_total(df):
return df['price'] * df['quantity'] * 1.1
start = time.time()
result1 = slow_calculate_total(df)
print(f"apply方式: {time.time() - start:.4f}秒")
start = time.time()
result2 = fast_calculate_total(df)
print(f"向量化: {time.time() - start:.4f}秒")
# 典型输出:
# apply方式: 2.3456秒
# 向量化: 0.0123秒 # 快190倍!
# 使用where/mask进行条件赋值
# 慢速版本
def slow_categorize(df):
def categorize(price):
if price < 100:
return '低'
elif price < 500:
return '中'
else:
return '高'
return df['price'].apply(categorize)
# 快速版本
def fast_categorize(df):
return pd.cut(df['price'],
bins=[0, 100, 500, float('inf')],
labels=['低', '中', '高'])
使用局部变量和缓存
# 优化前:频繁访问属性和全局变量
class SlowProcessor:
def __init__(self):
self.threshold = 100
self.multiplier = 1.5
def process(self, data):
result = []
for item in data:
# 每次循环都要查找self.threshold
if item > self.threshold:
result.append(item * self.multiplier)
return result
# 优化后:使用局部变量
class FastProcessor:
def __init__(self):
self.threshold = 100
self.multiplier = 1.5
def process(self, data):
# 局部变量访问更快
threshold = self.threshold
multiplier = self.multiplier
result = []
append = result.append # 缓存方法引用
for item in data:
if item > threshold:
append(item * multiplier)
return result
# 使用functools.lru_cache缓存结果
from functools import lru_cache
@lru_cache(maxsize=128)
def expensive_calculation(n):
"""耗时计算,结果会被缓存"""
return sum(i ** 2 for i in range(n))
# 第一次调用:慢
result1 = expensive_calculation(10000)
# 第二次调用相同参数:快(从缓存取)
result2 = expensive_calculation(10000)
性能优化检查清单:
✅ 使用合适的数据结构
- 查找操作用
set或dict(O(1))而不是list(O(n)) - 需要频繁插入删除用
collections.deque
✅ 避免不必要的拷贝
- 使用切片视图而不是复制
- 大对象传递使用引用
✅ 使用生成器节省内存
- 处理大数据时用生成器表达式而不是列表推导式
✅ 批量操作
- 数据库批量插入而不是逐条插入
- 批量API请求
✅ 使用内置函数和库
sum(),max(),min()等内置函数比循环快- NumPy、Pandas的向量化操作
性能分析工具
# 使用cProfile分析性能瓶颈
import cProfile
import pstats
profiler = cProfile.Profile()
profiler.enable()
# 运行你的代码
your_function()
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(10) # 打印最慢的10个函数
▶ 如何实现多线程/多进程数据处理?
处理大量数据时,合理使用并发可以大幅提升效率。
多线程 vs 多进程对比:
| 特性 | 多线程(Threading) | 多进程(Multiprocessing) |
|---|---|---|
| 适用场景 | IO密集型(网络请求、文件读写) | CPU密集型(计算、数据处理) |
| GIL影响 | 受GIL限制,无法真正并行 | 独立进程,不受GIL限制 |
| 内存 | 共享内存,占用小 | 独立内存,占用大 |
| 通信 | 简单(共享变量) | 复杂(需要进程间通信) |
| 启动速度 | 快 | 慢(需要fork进程) |
| 资源开销 | 小 | 大 |
| 数据安全 | 需要加锁 | 天然隔离,更安全 |
| 使用模块 | threading | multiprocessing |
| 典型案例 | 爬虫、API请求、文件上传 | 图像处理、数据分析、科学计算 |
场景:批量下载用户头像、处理大量文件、并行计算等。
多线程 - 适合IO密集型任务
import threading
import requests
import time
def download_image(url, save_path):
"""下载单个图片"""
try:
response = requests.get(url, timeout=10)
with open(save_path, 'wb') as f:
f.write(response.content)
print(f"下载完成: {save_path}")
except Exception as e:
print(f"下载失败 {url}: {e}")
# 方法1:手动创建线程
def download_images_threading(url_list):
"""多线程下载图片"""
threads = []
for idx, url in enumerate(url_list):
save_path = f"image_{idx}.jpg"
thread = threading.Thread(
target=download_image,
args=(url, save_path)
)
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
print("所有下载完成")
# 使用示例
urls = [
'https://example.com/image1.jpg',
'https://example.com/image2.jpg',
# ... 更多URL
]
start = time.time()
download_images_threading(urls)
print(f"总耗时: {time.time() - start:.2f}秒")
多进程 - 适合CPU密集型任务
import multiprocessing as mp
import numpy as np
def calculate_stats(data_chunk):
"""计算统计信息(CPU密集型)"""
return {
'mean': np.mean(data_chunk),
'std': np.std(data_chunk),
'min': np.min(data_chunk),
'max': np.max(data_chunk)
}
def parallel_calculate(data, num_processes=4):
"""多进程并行计算"""
# 分割数据
chunk_size = len(data) // num_processes
chunks = [
data[i:i+chunk_size]
for i in range(0, len(data), chunk_size)
]
# 创建进程池
with mp.Pool(processes=num_processes) as pool:
results = pool.map(calculate_stats, chunks)
return results
# 使用示例
if __name__ == '__main__':
# 生成大量数据
data = np.random.randn(10000000)
start = time.time()
results = parallel_calculate(data, num_processes=4)
print(f"多进程耗时: {time.time() - start:.2f}秒")
# 单进程对比
start = time.time()
result_single = calculate_stats(data)
print(f"单进程耗时: {time.time() - start:.2f}秒")
线程池 - 更优雅的线程管理
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
def fetch_user_data(user_id):
"""获取单个用户数据"""
try:
response = requests.get(
f'https://api.example.com/users/{user_id}',
timeout=5
)
return user_id, response.json()
except Exception as e:
return user_id, None
def batch_fetch_users(user_ids, max_workers=10):
"""批量获取用户数据"""
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 提交所有任务
future_to_id = {
executor.submit(fetch_user_data, uid): uid
for uid in user_ids
}
# 处理完成的任务
for future in as_completed(future_to_id):
user_id, data = future.result()
results[user_id] = data
print(f"完成: {user_id}")
return results
# 使用示例
user_ids = list(range(1, 101)) # 100个用户
results = batch_fetch_users(user_ids, max_workers=20)
# 带进度显示
from tqdm import tqdm
def batch_fetch_with_progress(user_ids, max_workers=10):
"""带进度条的批量获取"""
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
# 使用tqdm包装as_completed
futures = {
executor.submit(fetch_user_data, uid): uid
for uid in user_ids
}
for future in tqdm(as_completed(futures), total=len(user_ids)):
user_id, data = future.result()
results[user_id] = data
return results
进程池 - 数据处理任务
from concurrent.futures import ProcessPoolExecutor
import pandas as pd
def process_file(filepath):
"""处理单个文件"""
df = pd.read_csv(filepath)
# 数据清洗和统计
cleaned = df.dropna()
stats = {
'file': filepath,
'total_rows': len(df),
'valid_rows': len(cleaned),
'revenue_sum': cleaned['revenue'].sum() if 'revenue' in cleaned else 0
}
return stats
def process_multiple_files(file_list, max_workers=None):
"""并行处理多个文件"""
if max_workers is None:
max_workers = mp.cpu_count()
results = []
with ProcessPoolExecutor(max_workers=max_workers) as executor:
# map会按顺序返回结果
results = list(executor.map(process_file, file_list))
return results
# 使用示例
files = ['data1.csv', 'data2.csv', 'data3.csv', 'data4.csv']
results = process_multiple_files(files, max_workers=4)
# 汇总结果
total_revenue = sum(r['revenue_sum'] for r in results)
print(f"总收入: {total_revenue}")
选择指南:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 网络请求、文件IO | 多线程 / 线程池 | GIL不影响IO操作 |
| 大量计算 | 多进程 / 进程池 | 绕过GIL限制 |
| 简单任务 | ThreadPoolExecutor | API简单,易用 |
| 复杂任务 | ProcessPoolExecutor | 独立内存空间 |
注意事项
- 线程不适合CPU密集型:Python的GIL限制多线程CPU性能
- 进程开销大:创建进程比线程慢,适合长时间任务
- 共享数据需要同步:使用Lock、Queue等同步机制
- 注意资源限制:同时打开的文件数、网络连接数有上限
▶ 如何实现数据去重的不同策略?
数据去重是数据清洗的常见需求,不同场景需要不同策略。
场景:用户行为日志去重、订单数据去重等。
import pandas as pd
import hashlib
# 示例数据
data = pd.DataFrame({
'user_id': [1, 1, 2, 2, 3, 4, 4],
'action': ['login', 'login', 'purchase', 'purchase', 'logout', 'login', 'login'],
'timestamp': ['2024-01-01 10:00', '2024-01-01 10:00',
'2024-01-01 10:05', '2024-01-01 10:06',
'2024-01-01 10:10', '2024-01-01 10:15', '2024-01-01 10:15'],
'amount': [None, None, 100, 100, None, None, None]
})
# 策略1:完全相同的行去重
def remove_exact_duplicates(df):
"""移除完全相同的行"""
return df.drop_duplicates()
# 策略2:基于特定列去重
def remove_by_columns(df, subset):
"""基于指定列去重"""
# keep='first' 保留第一次出现的
# keep='last' 保留最后一次出现的
# keep=False 删除所有重复的
return df.drop_duplicates(subset=subset, keep='first')
# 策略3:基于时间窗口去重(短时间内的重复点击)
def remove_by_time_window(df, time_col, window='1s'):
"""移除时间窗口内的重复"""
df[time_col] = pd.to_datetime(df[time_col])
df = df.sort_values(time_col)
# 计算时间差
df['time_diff'] = df.groupby('user_id')[time_col].diff()
# 保留第一条和时间差大于窗口的记录
mask = (df['time_diff'].isna()) | (df['time_diff'] > pd.Timedelta(window))
result = df[mask].drop('time_diff', axis=1)
return result
# 策略4:自定义逻辑去重(保留金额大的)
def remove_by_custom_logic(df):
"""自定义去重逻辑"""
def keep_best(group):
# 如果有金额,保留金额最大的
if group['amount'].notna().any():
return group.loc[group['amount'].idxmax()]
else:
return group.iloc[0]
return df.groupby(['user_id', 'action'], as_index=False).apply(keep_best)
# 策略5:基于内容哈希去重(文本数据)
def remove_by_content_hash(df, text_column):
"""基于内容哈希去重"""
def hash_text(text):
return hashlib.md5(str(text).encode()).hexdigest()
df['content_hash'] = df[text_column].apply(hash_text)
df = df.drop_duplicates(subset=['content_hash'])
df = df.drop('content_hash', axis=1)
return df
# 策略6:保留最新数据
def keep_latest(df, id_col, time_col):
"""保留每个ID的最新记录"""
df[time_col] = pd.to_datetime(df[time_col])
return df.sort_values(time_col).groupby(id_col).tail(1)
# 使用示例
print("原始数据:")
print(data)
print("\n完全去重:")
print(remove_exact_duplicates(data))
print("\n基于user_id和action去重:")
print(remove_by_columns(data, subset=['user_id', 'action']))
print("\n时间窗口去重:")
print(remove_by_time_window(data.copy(), 'timestamp', window='1min'))
print("\n保留最新记录:")
print(keep_latest(data.copy(), 'user_id', 'timestamp'))
大数据去重技巧:
# 对于超大数据集,分块去重
def dedupe_large_file(input_file, output_file, chunksize=10000):
"""分块处理大文件去重"""
seen_ids = set()
first_chunk = True
for chunk in pd.read_csv(input_file, chunksize=chunksize):
# 过滤已见过的ID
mask = ~chunk['user_id'].isin(seen_ids)
new_data = chunk[mask]
# 更新seen_ids
seen_ids.update(new_data['user_id'].unique())
# 写入结果
new_data.to_csv(
output_file,
mode='a',
header=first_chunk,
index=False
)
first_chunk = False
return len(seen_ids)
# 使用Bloom Filter进行大规模去重
from pybloom_live import BloomFilter
def dedupe_with_bloom_filter(data_stream, capacity=1000000):
"""使用布隆过滤器去重(节省内存)"""
bf = BloomFilter(capacity=capacity, error_rate=0.001)
unique_items = []
for item in data_stream:
if item not in bf:
bf.add(item)
unique_items.append(item)
return unique_items
实战建议:
- 小数据集:直接用pandas的
drop_duplicates - 大数据集:分块处理 + 集合记录已见
- 超大数据集:使用Bloom Filter或外部排序
- 实时去重:使用Redis的Set数据结构
▶ 如何处理时间序列数据?
时间序列数据在用户行为分析、业务趋势分析中很常见。
场景:分析用户每日活跃度、计算留存率、预测销售趋势。
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# 创建示例时间序列数据
dates = pd.date_range('2024-01-01', periods=100, freq='D')
data = pd.DataFrame({
'date': dates,
'active_users': np.random.randint(1000, 2000, 100),
'revenue': np.random.uniform(5000, 10000, 100)
})
# 1. 时间索引
data.set_index('date', inplace=True)
# 2. 基本时间操作
# 按月聚合
monthly = data.resample('M').agg({
'active_users': 'mean',
'revenue': 'sum'
})
# 按周聚合
weekly = data.resample('W').agg({
'active_users': 'mean',
'revenue': 'sum'
})
# 3. 移动平均(平滑数据)
data['users_ma7'] = data['active_users'].rolling(window=7).mean() # 7日均值
data['users_ma30'] = data['active_users'].rolling(window=30).mean() # 30日均值
# 4. 环比和同比
data['revenue_change'] = data['revenue'].pct_change() # 日环比
data['revenue_change_7d'] = data['revenue'].pct_change(periods=7) # 周同比
# 5. 累计值
data['revenue_cumsum'] = data['revenue'].cumsum() # 累计收入
# 6. 季节性分解
from statsmodels.tsa.seasonal import seasonal_decompose
# 分解为趋势、季节性、残差
result = seasonal_decompose(data['active_users'], model='additive', period=7)
# 绘制分解结果
import matplotlib.pyplot as plt
fig, axes = plt.subplots(4, 1, figsize=(12, 8))
result.observed.plot(ax=axes[0], title='原始数据')
result.trend.plot(ax=axes[1], title='趋势')
result.seasonal.plot(ax=axes[2], title='季节性')
result.resid.plot(ax=axes[3], title='残差')
plt.tight_layout()
# 7. 计算留存率
def calculate_retention(df, user_col='user_id', date_col='date', periods=[1, 7, 14, 30]):
"""
计算用户留存率
df: 包含user_id和date的用户行为数据
periods: 要计算的留存天数
"""
df[date_col] = pd.to_datetime(df[date_col])
# 每个用户的首次行为日期
first_date = df.groupby(user_col)[date_col].min().reset_index()
first_date.columns = [user_col, 'first_date']
# 合并回原数据
df = df.merge(first_date, on=user_col)
# 计算距离首次的天数
df['days_since_first'] = (df[date_col] - df['first_date']).dt.days
# 计算各期留存
retention = {}
total_new_users = first_date[user_col].nunique()
for period in periods:
# 在period天后还活跃的用户数
retained = df[df['days_since_first'] >= period][user_col].nunique()
retention[f'Day {period}'] = retained / total_new_users * 100
return pd.Series(retention)
# 8. 时间窗口聚合
def rolling_stats(df, value_col, windows=[7, 14, 30]):
"""计算滚动统计指标"""
for window in windows:
df[f'{value_col}_mean_{window}d'] = df[value_col].rolling(window).mean()
df[f'{value_col}_std_{window}d'] = df[value_col].rolling(window).std()
df[f'{value_col}_min_{window}d'] = df[value_col].rolling(window).min()
df[f'{value_col}_max_{window}d'] = df[value_col].rolling(window).max()
return df
# 9. 异常检测
def detect_anomalies(df, column, window=7, threshold=3):
"""
基于移动平均和标准差检测异常
threshold: 几倍标准差视为异常
"""
rolling_mean = df[column].rolling(window).mean()
rolling_std = df[column].rolling(window).std()
# 计算z-score
z_score = (df[column] - rolling_mean) / rolling_std
# 标记异常
df['is_anomaly'] = np.abs(z_score) > threshold
return df
# 10. 时间特征工程
def extract_time_features(df, date_col):
"""提取时间特征用于模型训练"""
df[date_col] = pd.to_datetime(df[date_col])
df['year'] = df[date_col].dt.year
df['month'] = df[date_col].dt.month
df['day'] = df[date_col].dt.day
df['dayofweek'] = df[date_col].dt.dayofweek # 0=Monday
df['is_weekend'] = df['dayofweek'].isin([5, 6]).astype(int)
df['quarter'] = df[date_col].dt.quarter
df['week_of_year'] = df[date_col].dt.isocalendar().week
return df
# 使用示例
print("原始数据:")
print(data.head())
print("\n月度聚合:")
print(monthly)
print("\n添加移动平均:")
data_with_ma = rolling_stats(data.copy(), 'active_users')
print(data_with_ma[['active_users', 'active_users_mean_7d', 'active_users_mean_30d']].head(35))
print("\n异常检测:")
data_with_anomaly = detect_anomalies(data.copy(), 'revenue')
print(data_with_anomaly[data_with_anomaly['is_anomaly']])
时间序列分析要点:
数据预处理:
- 处理缺失时间点(插值、填充)
- 处理异常值
- 统一时间频率
探索性分析:
- 趋势分析(是否增长/下降)
- 季节性分析(周期性波动)
- 平稳性检验
特征工程:
- 滞后特征(lag features)
- 滚动统计特征
- 时间编码(周几、月份等)
预测建模:
- ARIMA模型
- Prophet(Facebook开源)
- LSTM(深度学习)
▶ 如何实现数据质量检查?
数据分析前需要对数据质量进行全面检查,确保数据可用性。
场景:接收外部数据源,需要验证数据完整性、准确性、一致性。
import pandas as pd
import numpy as np
from typing import Dict, List, Any
class DataQualityChecker:
"""数据质量检查器"""
def __init__(self, df: pd.DataFrame):
self.df = df
self.report = {
'basic_info': {},
'missing_values': {},
'duplicates': {},
'data_types': {},
'outliers': {},
'distributions': {},
'issues': []
}
def check_all(self):
"""执行所有检查"""
self.check_basic_info()
self.check_missing_values()
self.check_duplicates()
self.check_data_types()
self.check_outliers()
self.check_value_ranges()
self.check_distributions()
return self.get_report()
def check_basic_info(self):
"""基本信息检查"""
self.report['basic_info'] = {
'rows': len(self.df),
'columns': len(self.df.columns),
'memory_usage': f"{self.df.memory_usage(deep=True).sum() / 1024 ** 2:.2f} MB",
'column_names': self.df.columns.tolist()
}
def check_missing_values(self):
"""缺失值检查"""
missing = self.df.isnull().sum()
missing_pct = (missing / len(self.df)) * 100
self.report['missing_values'] = {
col: {
'count': int(missing[col]),
'percentage': f"{missing_pct[col]:.2f}%"
}
for col in self.df.columns if missing[col] > 0
}
# 警告:超过30%缺失
for col, info in self.report['missing_values'].items():
if float(info['percentage'].rstrip('%')) > 30:
self.report['issues'].append(
f"警告: 列 '{col}' 缺失率过高 ({info['percentage']})"
)
def check_duplicates(self):
"""重复值检查"""
total_dups = self.df.duplicated().sum()
self.report['duplicates'] = {
'total_duplicates': int(total_dups),
'percentage': f"{(total_dups / len(self.df)) * 100:.2f}%"
}
if total_dups > 0:
self.report['issues'].append(
f"发现 {total_dups} 行重复数据"
)
def check_data_types(self):
"""数据类型检查"""
self.report['data_types'] = {
col: str(dtype)
for col, dtype in self.df.dtypes.items()
}
# 检查数值列是否错误存储为字符串
for col in self.df.columns:
if self.df[col].dtype == 'object':
# 尝试转换为数值
try:
pd.to_numeric(self.df[col], errors='raise')
self.report['issues'].append(
f"建议: 列 '{col}' 可能应该是数值类型但存储为文本"
)
except:
pass
def check_outliers(self, method='iqr', threshold=1.5):
"""异常值检查"""
numeric_cols = self.df.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
Q1 = self.df[col].quantile(0.25)
Q3 = self.df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - threshold * IQR
upper_bound = Q3 + threshold * IQR
outliers = self.df[
(self.df[col] < lower_bound) |
(self.df[col] > upper_bound)
]
if len(outliers) > 0:
self.report['outliers'][col] = {
'count': len(outliers),
'percentage': f"{(len(outliers) / len(self.df)) * 100:.2f}%",
'range': f"[{self.df[col].min()}, {self.df[col].max()}]",
'expected_range': f"[{lower_bound:.2f}, {upper_bound:.2f}]"
}
def check_value_ranges(self):
"""值范围检查"""
# 自定义业务规则检查
# 例如:年龄应该在0-120之间
if 'age' in self.df.columns:
invalid_age = self.df[(self.df['age'] < 0) | (self.df['age'] > 120)]
if len(invalid_age) > 0:
self.report['issues'].append(
f"错误: 发现 {len(invalid_age)} 条年龄数据不合理"
)
# 价格不应该为负
price_cols = [col for col in self.df.columns if 'price' in col.lower()]
for col in price_cols:
if (self.df[col] < 0).any():
negative_count = (self.df[col] < 0).sum()
self.report['issues'].append(
f"错误: 列 '{col}' 发现 {negative_count} 个负值"
)
def check_distributions(self):
"""分布检查"""
numeric_cols = self.df.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
self.report['distributions'][col] = {
'mean': float(self.df[col].mean()),
'median': float(self.df[col].median()),
'std': float(self.df[col].std()),
'min': float(self.df[col].min()),
'max': float(self.df[col].max()),
'skewness': float(self.df[col].skew()),
'kurtosis': float(self.df[col].kurt())
}
def get_report(self):
"""生成报告"""
return self.report
def print_report(self):
"""打印报告"""
print("=" * 60)
print("数据质量检查报告")
print("=" * 60)
print("\n基本信息:")
for key, value in self.report['basic_info'].items():
print(f" {key}: {value}")
print("\n缺失值:")
if self.report['missing_values']:
for col, info in self.report['missing_values'].items():
print(f" {col}: {info['count']} ({info['percentage']})")
else:
print(" 无缺失值")
print("\n重复数据:")
print(f" {self.report['duplicates']['total_duplicates']} 行 "
f"({self.report['duplicates']['percentage']})")
print("\n异常值:")
if self.report['outliers']:
for col, info in self.report['outliers'].items():
print(f" {col}: {info['count']} ({info['percentage']})")
else:
print(" 未检测到明显异常值")
print("\n问题汇总:")
if self.report['issues']:
for issue in self.report['issues']:
print(f" - {issue}")
else:
print(" 未发现严重问题")
print("=" * 60)
# 使用示例
# 创建测试数据
test_data = pd.DataFrame({
'user_id': [1, 2, 3, 3, 4, 5], # 有重复
'age': [25, 30, None, 35, 150, -5], # 有缺失和异常值
'income': [5000, 6000, 7000, 8000, 100000, 4500], # 有异常值
'gender': ['M', 'F', 'M', 'F', 'M', 'F'],
'score': ['80', '90', '85', '75', '95', '88'] # 应该是数值但存为文本
})
# 执行检查
checker = DataQualityChecker(test_data)
report = checker.check_all()
# 打印报告
checker.print_report()
# 也可以获取详细报告进行程序化处理
print("\n详细报告(JSON格式):")
import json
print(json.dumps(report, indent=2, ensure_ascii=False))
数据质量维度:
- 完整性:数据是否完整,有无缺失
- 准确性:数据是否准确,有无错误
- 一致性:数据是否一致,有无矛盾
- 及时性:数据是否及时,有无过期
- 唯一性:数据是否唯一,有无重复
- 有效性:数据是否有效,是否符合业务规则
LLM基础概念
▶ 什么是LLM(大语言模型)?
LLM(Large Language Model,大语言模型)是基于Transformer架构、通过大规模文本数据训练的深度学习模型,能够理解和生成人类语言。
核心特征:
规模庞大:参数量通常在数十亿到数千亿级别
- GPT-3:175B参数
- GPT-4:据估计超过1T参数
- Claude 3:参数量未公开,但能力超越GPT-3.5
预训练 + 微调:
- 预训练:在大规模无标注文本上学习语言规律
- 微调:针对特定任务进行优化(如对话、编程、分析等)
涌现能力(Emergent Abilities):
- 当模型规模达到一定程度,会自然涌现出一些未被明确训练的能力
- 如:逻辑推理、代码生成、数学计算、多语言理解等
主要能力:
- 自然语言理解:理解文本含义、上下文、情感等
- 文本生成:创作文章、对话、代码、翻译等
- 知识问答:回答各领域问题
- 推理能力:逻辑推理、数学推理、因果推理
- 任务适应:通过Prompt快速适应新任务
常见的LLM对比:
| 厂商 | 模型系列 | 主要特点 | 典型应用 |
|---|---|---|---|
| OpenAI | GPT-3.5、GPT-4、GPT-4 Turbo | 综合能力强、API稳定 | ChatGPT、代码辅助、内容生成 |
| Anthropic | Claude 3 (Haiku/Sonnet/Opus) | 安全性高、上下文长、推理强 | 复杂任务、长文档分析 |
| Gemini、PaLM 2 | 多模态、搜索整合 | 搜索增强、图文理解 | |
| Meta | Llama 2、Llama 3 | 开源、可本地部署 | 私有化部署、研究 |
| 国内 | 文心一言、通义千问、智谱GLM、Kimi | 中文优化、长文本 | 中文场景、知识问答 |
应用场景:
- 智能客服、内容创作、代码辅助
- 数据分析、知识问答、教育辅导
- 文本摘要、翻译、信息抽取
▶ 什么是Prompt Engineering(提示词工程)?
Prompt Engineering是设计和优化输入提示词(Prompt)以获得更好LLM输出的技术和方法。
为什么需要Prompt Engineering:
LLM的输出质量高度依赖于输入的Prompt质量:
- 不清晰的Prompt → 模糊、不准确的回答
- 精心设计的Prompt → 精准、高质量的输出
Prompt的基本要素:
Role(角色)
明确AI的角色定位:
你是一位资深的数据分析师,擅长用户行为分析和AB测试...
作用:
- 设定回答的视角和专业度
- 约束回答的范围和深度
Task(任务)
清晰描述要完成的任务:
请分析这组用户行为数据,找出影响留存率的关键因素...
要点:
- 具体、明确
- 可执行、可衡量
- 一次只做一个主要任务
Context(背景)
提供必要的上下文信息:
背景:某社交APP在10月上线新版本后,次日留存率从40%降到28%
数据:附带用户行为数据CSV文件
目标:找到留存下降的原因
作用:
- 帮助模型更好理解问题
- 提供决策依据
Format(格式)
指定输出格式:
请按照以下格式输出:
1. 数据概览(3-5个核心指标)
2. 问题分析(分点说明)
3. 改进建议(具体可行的方案)
4. 预期效果(量化预估)
作用:
- 结构化输出,易于理解
- 确保包含关键信息
Prompt优化技巧:
- Few-Shot Learning(少样本学习)
提供示例引导模型:
示例1:
输入:[数据]
输出:[期望的分析]
示例2:
输入:[数据]
输出:[期望的分析]
现在请分析:[实际数据]
- Chain of Thought(思维链)
引导模型一步步思考:
让我们一步步分析:
1. 首先,计算各环节的转化率
2. 然后,识别转化率异常的环节
3. 接下来,分析可能的原因
4. 最后,提出优化建议
- 明确约束条件
要求:
- 答案不超过500字
- 用数据支撑结论
- 提供3个具体可行的方案
- 不要使用专业术语,用通俗语言解释
常见问题与优化:
Prompt优化前后对比
❌ 不好的Prompt: “分析这个数据”
问题:过于模糊,缺少目标和约束
✅ 优化后的Prompt:
你是一位数据分析专家。请分析以下用户购买数据,完成以下任务:
1. 计算整体转化率和各环节转化率
2. 找出转化率最低的环节(流失点)
3. 分析该环节流失的可能原因
4. 提出3个具体的优化建议
要求:
- 用数据支撑结论
- 建议需要可执行、可衡量
- 输出格式为Markdown表格
进阶技巧:
- Self-Consistency:多次生成,选择最一致的答案
- ReAct:推理(Reasoning)+ 行动(Acting)交替进行
- Tree of Thoughts:探索多个思维路径,选择最优解
▶ 什么是RAG(检索增强生成)?
RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合信息检索和文本生成的技术,通过从外部知识库检索相关信息来增强LLM的生成能力。
为什么需要RAG:
LLM面临的局限:
- 知识截止日期:训练数据有时间限制,无法获取最新信息
- 幻觉问题:可能编造不存在的信息
- 领域知识不足:对特定领域(如企业内部数据)了解有限
- 无法引用来源:难以验证信息的准确性
RAG的优势:
- 提供最新信息和特定领域知识
- 减少幻觉,提高准确性
- 可追溯信息来源
- 无需重新训练模型
RAG的基本流程:
用户问题
↓
1. 查询理解(问题改写、关键词提取)
↓
2. 检索相关文档(向量检索、关键词检索)
↓
3. 文档重排序(选择最相关的top-k)
↓
4. 构建Prompt(问题 + 检索到的上下文)
↓
5. LLM生成答案
↓
6. 后处理(格式化、引用来源)
↓
答案输出
RAG的核心组件:
向量数据库(Vector DB)
存储文档的向量表示:
常用向量数据库:
- Pinecone(云服务)
- Milvus(开源)
- Weaviate(开源)
- Chroma(轻量级)
- FAISS(Facebook开源)
作用:
- 高效存储和检索向量
- 支持相似度搜索
- 支持大规模数据
Embedding模型
将文本转换为向量:
常用模型:
- OpenAI Embedding(text-embedding-3-small/large)
- BGE系列(BAAI/bge-large-zh)
- M3E(moka-ai/m3e-base)
作用:
- 将文本映射到高维向量空间
- 语义相似的文本距离近
检索策略
如何找到相关文档:
稠密检索(Dense Retrieval)
- 使用向量相似度(余弦相似度)
- 捕捉语义相似性
- 效果好但计算成本高
稀疏检索(Sparse Retrieval)
- BM25等关键词匹配
- 精确匹配关键词
- 速度快但语义理解弱
混合检索(Hybrid)
- 结合稠密和稀疏检索
- 取长补短
生成策略
如何利用检索结果:
Prompt模板:
基于以下上下文回答问题:
上下文1: [检索到的文档1]
上下文2: [检索到的文档2]
上下文3: [检索到的文档3]
问题:{用户问题}
要求:
- 只根据上下文回答,不要编造信息
- 如果上下文不包含答案,请明确说明
- 请引用具体的上下文来源
RAG的优化技巧:
文档分块(Chunking)
- 合理的chunk大小(通常512-1024 tokens)
- 保持语义完整性
- 添加重叠(overlap)避免信息割裂
查询优化
- 问题改写:将复杂问题拆解
- 关键词提取:提取核心概念
- 多查询:从不同角度检索
重排序(Reranking)
- 使用专门的重排序模型
- 根据相关性对检索结果重新排序
- 提升top-k结果的质量
混合检索
- 结合向量检索和关键词检索
- 利用各自优势
RAG的应用场景:
- 企业知识库问答:基于内部文档回答员工问题
- 客户服务:基于产品文档和FAQ回答用户问题
- 法律/医疗咨询:基于专业文献提供建议
- 代码助手:基于代码库回答编程问题
RAG的挑战
- 检索质量:如果检索不到相关文档,生成质量会下降
- 上下文长度限制:LLM的上下文窗口有限,不能塞入太多文档
- 延迟问题:检索 + 生成增加了响应时间
- 成本:需要维护向量数据库和Embedding服务
▶ 什么是AI Agent(智能体)?
AI Agent是能够感知环境、自主决策并执行行动以达成目标的智能系统。相比传统LLM的单次问答,Agent能够进行多步推理、使用工具、并迭代优化以完成复杂任务。
Agent的核心能力:
- 感知(Perception):理解用户意图和环境状态
- 规划(Planning):制定达成目标的行动计划
- 执行(Execution):调用工具和API执行具体操作
- 反思(Reflection):评估结果,调整策略
- 记忆(Memory):存储历史信息,支持长期交互
Agent的基本架构:
用户输入
↓
┌─────────────────────┐
│ LLM核心引擎 │
│ (推理与决策) │
└─────────────────────┘
↓ ↓
计划制定 工具调用
↓ ↓
┌─────────────────────┐
│ 工具箱 (Tools) │
│ - 搜索引擎 │
│ - 计算器 │
│ - 数据库查询 │
│ - API调用 │
│ - 文件操作 │
└─────────────────────┘
↓
┌─────────────────────┐
│ 记忆系统 │
│ - 短期记忆(对话) │
│ - 长期记忆(知识) │
└─────────────────────┘
↓
结果输出
Agent vs LLM对话:
传统LLM对话
特点:
- 单轮问答
- 只能基于训练数据回答
- 无法执行实际操作
- 无法获取实时信息
示例:
用户:帮我查一下明天深圳的天气
LLM:抱歉,我无法获取实时天气信息,
我的训练数据截止到2023年...
AI Agent
特点:
- 多步推理和规划
- 可以调用工具
- 可以执行实际操作
- 可以获取实时信息
示例:
用户:帮我查一下明天深圳的天气
Agent思考过程:
1. 理解意图:查询天气
2. 制定计划:调用天气API
3. 执行:search_weather("深圳", "明天")
4. 返回结果:
"明天深圳天气:晴,22-28°C,
建议穿着轻便..."
常见的Agent框架:
- ReAct(Reasoning + Acting)
交替进行推理和行动:
Thought: 我需要先查询用户的订单信息
Action: query_database(user_id=123)
Observation: 用户有3个订单,最近一个是...
Thought: 基于订单信息,我可以回答用户的问题了
Action: generate_response(...)
- AutoGPT类
自主规划和执行多步任务:
- 分解任务为子任务
- 逐步执行并验证
- 自我纠错和优化
- Multi-Agent系统
多个Agent协作完成复杂任务:
- 专家Agent:各司其职(如数据分析Agent、写作Agent)
- 管理Agent:协调和分配任务
- 评估Agent:质量控制
Agent的应用场景:
- 数据分析助手:自动获取数据、分析、生成报告
- 客户服务:查询订单、处理退款、解答问题
- 代码助手:理解需求、编写代码、调试、测试
- 研究助手:文献搜索、信息提取、报告生成
- 个人助理:日程管理、邮件处理、信息整理
Agent的工具(Tools):
常见工具类型
搜索工具:
- 网页搜索(Google、Bing)
- 知识库搜索
- 文档搜索
计算工具:
- 计算器
- Python解释器
- 数据分析工具
数据工具:
- SQL查询
- API调用
- 数据库操作
文件工具:
- 读写文件
- 文档解析(PDF、Word)
- 图片处理
通信工具:
- 发送邮件
- 消息通知
- API接口调用
Agent的挑战:
- 可靠性:多步推理可能出错,需要错误处理机制
- 成本:多次调用LLM,成本较高
- 安全性:需要限制Agent的权限,防止误操作
- 可控性:确保Agent按预期行为,不"失控"
▶ 什么是Fine-tuning(微调)?
Fine-tuning是在预训练模型的基础上,使用特定领域的数据继续训练,使模型适应特定任务或风格的过程。
为什么需要Fine-tuning:
- 提升特定任务表现:在垂直领域(如医疗、法律)效果更好
- 定制输出风格:符合企业的语气、格式要求
- 减少Prompt长度:通过训练固化行为,减少每次推理的Prompt
- 提高效率:小模型微调后可以达到大模型的效果
Fine-tuning的类型:
Full Fine-tuning(全量微调)
更新模型的所有参数:
特点:
- 效果最好
- 计算成本高(需要大量GPU)
- 需要大量训练数据(通常数千到数万条)
适用场景:
- 有充足资源和数据
- 对效果要求极高
- 领域差异很大
PEFT(参数高效微调)
只更新部分参数:
方法:
- Adapter:在模型中插入小模块
- Prefix-tuning:只训练前缀向量
- Prompt-tuning:优化连续的Prompt
优点:
- 参数量小,训练快
- 多任务切换方便
- 降低成本
LoRA(低秩适应)
最流行的PEFT方法:
原理:
- 冻结原模型参数
- 添加低秩矩阵进行训练
- 训练参数量仅为原模型的0.1-1%
优点:
- 显著降低训练成本
- 可以并行训练多个LoRA
- 推理时几乎无额外开销
Fine-tuning的流程:
数据准备
- 收集特定领域的高质量数据
- 格式化为训练格式(通常是问答对)
- 数据清洗和质量控制
选择基座模型
- 根据任务选择合适的预训练模型
- 考虑模型大小、语言、许可证
训练
- 设置超参数(学习率、batch size等)
- 监控训练过程,防止过拟合
- 在验证集上评估效果
评估与迭代
- 在测试集上评估效果
- 根据效果调整数据和参数
- 迭代优化
Fine-tuning vs Prompt Engineering vs RAG:
| 维度 | Prompt Engineering | RAG | Fine-tuning |
|---|---|---|---|
| 成本 | 低 | 中 | 高 |
| 效果 | 一般 | 好 | 最好 |
| 灵活性 | 高 | 中 | 低 |
| 数据需求 | 少 | 中 | 多 |
| 适用场景 | 通用任务 | 知识问答 | 专业领域 |
选择建议:
- 简单任务 → Prompt Engineering
- 需要最新/专业知识 → RAG
- 特定领域深度定制 → Fine-tuning
- 综合方案 → RAG + Fine-tuning
▶ 什么是MCP(模型上下文协议)?
MCP(Model Context Protocol)是Anthropic提出的一个开放协议标准,用于在AI应用和数据源之间建立标准化的连接方式,使AI模型能够安全、高效地访问外部数据和工具。
MCP解决的问题:
传统AI应用接入数据源的痛点:
- 碎片化:每个数据源需要单独集成
- 重复开发:不同AI应用都要重新实现相同的集成
- 维护困难:数据源变化需要更新所有集成
- 安全问题:没有统一的权限和安全标准
MCP的核心理念:
就像USB协议统一了设备连接,MCP旨在统一AI与数据源的连接:
传统方式:
AI App 1 ──→ 数据源A的专有接口
AI App 1 ──→ 数据源B的专有接口
AI App 2 ──→ 数据源A的专有接口(重复开发)
AI App 2 ──→ 数据源B的专有接口(重复开发)
MCP方式:
AI App 1 ──→ MCP协议 ←── 数据源A的MCP Server
AI App 2 ──→ MCP协议 ←── 数据源B的MCP Server
MCP的架构:
MCP Host(宿主)
运行AI应用的环境:
- Claude Desktop
- IDE(如Cursor、VS Code)
- 自定义AI应用
职责:
- 管理与多个MCP Server的连接
- 协调资源和权限
- 提供用户界面
MCP Server(服务器)
数据源的标准化接口:
示例:
- 文件系统MCP Server
- 数据库MCP Server
- Git MCP Server
- Web搜索MCP Server
职责:
- 暴露数据和工具
- 实现MCP协议
- 处理权限和安全
MCP Client(客户端)
AI应用中的连接组件:
职责:
- 发现和连接MCP Server
- 发送请求和接收响应
- 管理会话状态
MCP的核心概念:
Resources(资源)
- 数据源暴露的数据
- 如:文件、数据库记录、API端点
Tools(工具)
- AI可以调用的操作
- 如:搜索、写入文件、执行查询
Prompts(提示模板)
- 预定义的提示词模板
- 帮助用户快速使用常见功能
MCP的优势:
- 标准化:统一的接口,降低集成复杂度
- 复用性:一次开发,多处使用
- 安全性:统一的权限和安全控制
- 可扩展:轻松添加新的数据源和工具
MCP的应用场景:
- 企业知识库集成:AI访问公司文档、数据库
- 开发工具集成:AI访问代码仓库、终端、文件系统
- 数据分析:AI访问数据仓库、BI工具
- 自动化工作流:AI调用各种第三方服务
实际例子:
在Cursor中使用MCP访问文件系统:
用户:帮我分析这个项目的代码结构
AI(通过MCP):
1. 调用filesystem MCP Server列出文件
2. 读取关键文件内容
3. 分析并总结代码结构
4. 生成结构图和说明
MCP的发展状态
MCP是2024年提出的新协议,目前还在早期阶段:
- Anthropic的Claude Desktop已支持
- 社区在积极开发各种MCP Server
- 未来可能成为AI应用的标准协议
类比:MCP希望成为AI领域的"USB协议"或"HTTP协议"
进阶概念
▶ 什么是Token?如何计算Token数量?
Token是LLM处理文本的基本单位,可以理解为文本的"原子"。
Token的本质:
- 不是完整的单词,也不是单个字符
- 而是常见的字符序列(subword)
- 通过分词算法(如BPE)将文本切分
Token的示例:
中文:
"数据分析" → ["数据", "分析"] (2个token)
"我爱编程" → ["我", "爱", "编", "程"] (4个token)
英文:
"Hello world" → ["Hello", " world"] (2个token)
"ChatGPT" → ["Chat", "GPT"] (2个token)
为什么不是按字符或单词:
- 按字符:太细粒度,序列太长,训练和推理效率低
- 按单词:词表太大,无法处理未见过的词
- Token(subword):平衡了效率和灵活性
Token数量估算:
粗略估算:
- 中文:1个汉字 ≈ 1-2个token
- 英文:1个单词 ≈ 1-2个token
- 代码:因缩进和符号,通常比自然语言多
精确计算:
- 使用OpenAI的tiktoken库
- 或各模型提供的tokenizer
Token的重要性:
成本计算:
- API按token计费
- 输入token + 输出token = 总成本
长度限制:
- 每个模型有最大token限制
- GPT-3.5: 4K/16K
- GPT-4: 8K/32K/128K
- Claude 3: 200K
性能影响:
- Token越多,推理越慢
- Token越多,成本越高
▶ 什么是Temperature和Top-p采样?
Temperature和Top-p是控制LLM生成文本随机性和多样性的两个重要参数。
Temperature(温度):
控制输出的"创造性"vs"确定性":
Temperature = 0:完全确定,总是选择概率最高的token
- 适合:事实问答、代码生成、数据分析
- 特点:输出稳定、可预测
Temperature = 0.7(默认):平衡创造性和稳定性
- 适合:对话、内容创作
Temperature = 1.0-2.0:高创造性,输出更随机
- 适合:创意写作、头脑风暴
- 特点:输出多样、有时不够精确
工作原理:
调整概率分布的"陡峭程度"
Temperature越低 → 分布越陡峭 → 更确定
Temperature越高 → 分布越平缓 → 更随机
Top-p(核采样):
也称为Nucleus Sampling,控制候选token的范围:
Top-p = 0.1:只考虑累计概率前10%的token
- 输出更集中、更保守
Top-p = 0.9(常用):考虑累计概率前90%的token
- 平衡多样性和质量
Top-p = 1.0:考虑所有token
- 最大多样性
Temperature vs Top-p:
Temperature单独使用
适合场景:
- 需要精确控制随机程度
- 简单场景
设置建议:
- 事实性任务:0-0.3
- 对话:0.7-0.9
- 创作:1.0-1.5
Top-p单独使用
适合场景:
- 动态调整候选范围
- 避免低质量输出
设置建议:
- 保守:0.5-0.7
- 平衡:0.9
- 开放:0.95-1.0
组合使用
最佳实践:
- Temperature=0.7, Top-p=0.9(通用设置)
- Temperature=0, Top-p=1.0(确定性任务)
- Temperature=1.0, Top-p=0.95(创造性任务)
注意:
- 两个参数都设置很低可能过于保守
- 两个参数都设置很高可能输出质量低
参数调优建议
不同任务的推荐设置:
数据分析:
- Temperature: 0-0.3
- Top-p: 0.9
- 原因:需要精确、客观的分析
代码生成:
- Temperature: 0-0.2
- Top-p: 0.9
- 原因:代码需要准确性
创意写作:
- Temperature: 0.8-1.2
- Top-p: 0.95
- 原因:需要多样性和创造力
客户服务:
- Temperature: 0.5-0.7
- Top-p: 0.9
- 原因:平衡准确性和自然度
▶ 什么是Embedding(向量嵌入)?
Embedding是将文本、图片等非结构化数据转换为固定维度向量的技术,使计算机能够"理解"和比较语义相似度。
为什么需要Embedding:
计算机无法直接理解文本含义:
- “猫"和"狗"对计算机只是不同的字符串
- 无法判断"机器学习"和"人工智能"是相关概念
Embedding将文本映射到向量空间:
- 语义相似的文本在向量空间中距离近
- 可以进行数学运算和相似度计算
Embedding的表示:
示例(简化为3维):
"数据分析" → [0.8, 0.3, 0.1]
"数据科学" → [0.7, 0.4, 0.2] # 距离近,语义相似
"篮球比赛" → [0.1, 0.2, 0.9] # 距离远,语义不相关
实际模型:
- OpenAI Embedding: 1536维
- BGE-large: 1024维
- M3E: 768维
相似度计算:
常用方法:余弦相似度
similarity = cos(θ) = (A·B) / (||A|| × ||B||)
值范围:-1到1
- 1:完全相同
- 0:无关
- -1:完全相反
Embedding的应用:
语义搜索
- 将查询和文档都转为向量
- 计算相似度,返回最相关文档
推荐系统
- 将用户和物品转为向量
- 推荐相似物品
聚类分析
- 将文本转为向量
- 使用K-Means等算法聚类
RAG系统
- 核心组件
- 用于文档检索
常用Embedding模型:
OpenAI Embedding
模型:
- text-embedding-3-small(1536维)
- text-embedding-3-large(3072维)
优点:
- 效果好
- API简单
缺点:
- 需要付费
- 数据上传到OpenAI
开源中文模型
推荐:
- BGE系列(BAAI/bge-large-zh)
- M3E(moka-ai/m3e-base)
- Text2vec
优点:
- 免费
- 可本地部署
- 中文效果好
开源多语言模型
推荐:
- sentence-transformers/all-MiniLM-L6-v2
- sentence-transformers/paraphrase-multilingual-mpnet-base-v2
优点:
- 支持多语言
- 社区活跃
Embedding的实践建议:
选择合适的模型
- 根据语言、领域、性能要求选择
- 中文任务优先考虑专门的中文模型
文档切分
- Embedding前要合理切分文档
- 保持语义完整性
- 通常512-1024 tokens per chunk
缓存Embedding
- Embedding计算耗时
- 对不变的文本缓存结果
混合检索
- Embedding检索(语义)+ 关键词检索(精确)
- 效果更好
数据分析基础
▶ 数据分析思维是什么?怎么去验证?
数据分析思维是使用数据分析技能解决问题
通过三个问题验证是否具备数据分析思维:
- 到底要怎么分析问题?
- 到底分析出什么结论?
- 这个结论有什么用?
业务指标与模型
▶ 什么是RFM模型?如何应用?
RFM模型是一种经典的用户价值评估模型,通过三个维度对用户进行分群和价值评估:
三个维度:
R (Recency - 最近一次消费):距离最后一次购买的时间,值越小越好
- 反映用户的活跃度和流失风险
- 最近购买的用户更可能再次购买
F (Frequency - 消费频次):一定时间内的购买次数
- 反映用户的忠诚度和粘性
- 频次越高说明用户越依赖产品
M (Monetary - 消费金额):一定时间内的总消费金额
- 反映用户的价值贡献
- 金额越高用户价值越大
评分方法:
将每个维度按分位数划分为5个等级(1-5分),组合成RFM评分:
- 如555表示:最近购买、高频次、高金额 → 最有价值客户
- 如155表示:最近购买、低频次、低金额 → 新用户/潜力客户
- 如511表示:很久未购买、高频次、低金额 → 流失预警
用户分群策略:
RFM用户分群对比:
| 用户类型 | RFM特征 | 用户画像 | 运营策略 | 典型案例 |
|---|---|---|---|---|
| 重要价值客户 | R高F高M高``(555、554、544) | 最近消费、购买频繁、金额高 | • VIP专属服务• 优先体验新品• 个性化推荐``• 忠诚度奖励 | 品牌忠实用户 |
| 重要发展客户 | R高M高F中低``(525、535、515) | 最近消费、金额高、但频次不高 | • 提升购买频次• 会员权益吸引• 定期触达提醒``• 建立购买习惯 | 新注册大额消费用户 |
| 重要挽留客户 | R低F高M高``(255、155、154) | 很久未消费、但曾经高价值 | • 流失预警• 大力度召回(优惠券)• 问卷调研流失原因``• 重建品牌连接 | 沉睡的老VIP |
| 一般用户 | 中等分值``(333、233、323) | 各项指标中等 | • 促进活跃• 提升客单价• 交叉销售``• 常规营销 | 普通消费者 |
| 新用户 | R高F低M低``(511、521、522) | 最近首次消费、金额小 | • 新手引导• 首单优惠• 培养忠诚度 | 刚注册用户 |
| 流失用户 | R低F低M低``(111、112、121) | 很久未消费、历史价值也低 | • 成本控制• 低成本触达• 放弃或重激活 | 僵尸用户 |
详细案例:
重要价值客户(RFM都高)
特征:555、554、544、545等
- 最近有消费
- 购买频繁
- 消费金额高
运营策略:
- 提供VIP专属服务
- 优先体验新品
- 个性化推荐
- 忠诚度奖励计划
重要发展客户(R高M高F中低)
特征:525、535、515等
- 最近有消费
- 消费金额高
- 但频次不高
运营策略:
- 提升购买频次
- 会员权益吸引
- 定期触达提醒
- 建立购买习惯
重要挽留客户(R低FM高)
特征:255、155、154等
- 很久未消费
- 但曾经是高价值用户
运营策略:
- 流失预警
- 大力度召回(优惠券)
- 问卷调研流失原因
- 重建品牌连接
一般用户(中等分值)
特征:333、233、323等
- 各项指标中等
运营策略:
- 促进活跃
- 提升客单价
- 交叉销售
- 常规营销
应用场景:
- 精准营销:针对不同群体设计营销活动
- 用户预警:识别即将流失的高价值用户
- 资源分配:将资源集中在高价值用户上
- 效果评估:追踪运营活动后用户的RFM变化
▶ 什么是漏斗分析?如何找到关键流失点?
漏斗分析是将用户完成某个目标的多步骤流程可视化,分析每个环节的转化率和流失率,找出优化机会。
漏斗分析的核心指标:
- 转化率:从上一步到下一步的用户转化比例
- 流失率:在某个环节流失的用户比例 = 1 - 转化率
- 整体转化率:从第一步到最后一步的总转化率
- 平均转化时长:用户完成整个流程的平均时间
典型的电商购买漏斗:
首页访问 (100%)
↓ 45%转化
商品浏览 (45%)
↓ 40%转化
加入购物车 (18%)
↓ 50%转化
提交订单 (9%)
↓ 67%转化
支付成功 (6%)
找到关键流失点的方法:
环节转化率对比
- 计算每个环节的转化率
- 识别转化率异常低的环节
- 如上例中"加购→下单"转化率仅50%,是最大流失点
流失用户数对比
- 计算每个环节的绝对流失用户数
- 流失用户数 = 上一环节人数 × (1 - 转化率)
- 优先优化流失用户数最多的环节
行业benchmark对比
- 与行业平均水平对比
- 低于行业水平的环节需要重点关注
用户行为分析
- 分析流失用户在该环节的行为
- 查看运费、反复查看价格、长时间停留等行为
- 推断流失原因
优化策略:
漏斗优化的优先级判断
考虑三个因素:
- 流失比例:流失率越高优先级越高
- 影响规模:流失绝对人数越多优先级越高
- 优化成本:改进成本越低优先级越高
综合评估:影响 = 流失人数 × 人均价值 - 优化成本
时间窗口设置:
不同场景需要设置不同的时间窗口:
- 实时漏斗(1小时内):适用于短流程,如注册、支付
- 会话漏斗(单次访问):适用于一次完成的流程
- 天级漏斗(1-7天):适用于需要考虑时间的长决策流程
时间窗口过短会低估转化率,过长会高估转化率
▶ 什么是留存率?如何分析留存曲线?
留存率是衡量产品粘性和用户价值的核心指标,反映用户在一段时间后继续使用产品的比例。
留存率定义:
N日留存率 = 第N天仍活跃的用户数 / 第0天的新增用户数 × 100%
常见留存指标:
- 次日留存(D1 Retention):第1天回访的用户比例,反映首日体验
- 3日留存(D3):第3天回访比例,反映短期粘性
- 7日留存(D7):第7天回访比例,进入第一个稳定期
- 30日留存(D30):第30天回访比例,反映长期价值
- 周留存:第N周的回访比例
- 月留存:第N月的回访比例
留存曲线分析:
一个典型的留存曲线呈现几个阶段:
100% (D0新增)
↓
42% (D1) - 快速流失期
↓
30% (D3) - 第一平台期
↓
22% (D7) - 第二下降期
↓
12% (D30) - 稳定期
关键节点分析:
0-3天:快速流失期
特点:流失率最高(50-70%) 原因:
- 产品不符合预期
- 未体验到核心价值
- 竞品分流
优化方向:
- 强化新手引导
- 突出核心价值
- 降低使用门槛
- 首日激励机制
3-7天:第一平台期
特点:流失趋缓,进入第一个稳定期 原因:
- 用户开始养成习惯
- 但习惯尚不稳固
优化方向:
- Push提醒
- 每日签到
- 新手任务
- 社交绑定
7-30天:稳定期形成
特点:留存趋于平稳 原因:
- 用户已形成使用习惯
- 这部分是产品核心用户
优化方向:
- 提升活跃度
- 付费转化
- 社交传播
- 会员权益
Magic Number分析
寻找与高留存强相关的关键行为:
经典案例:
- Facebook:10天内添加7个好友
- Twitter:关注30个账号
- Dropbox:上传1个文件
方法:
- 统计不同行为次数的用户留存率
- 找到留存率显著提升的拐点
- 引导用户完成该行为
留存率的重要性:
用户生命周期价值(LTV)与留存率直接相关:
LTV = ARPU × 平均留存月数
如果月留存率从20%提升到30%:
- 平均留存月数从1.25月增加到1.43月
- LTV提升14%
留存率优化建议
分群分析留存:不同用户群体的留存差异很大
- 新老用户留存对比
- 不同渠道用户留存对比
- 不同行为用户留存对比
追踪Cohort留存:按注册时间分组追踪
- 观察产品迭代对留存的影响
- 避免整体留存率的季节性误导
关注留存曲线形态:
- 微笑曲线:初期流失后回升 → 好产品
- 快速下降后平稳 → 正常产品
- 持续下降 → 产品有问题
▶ AARRR增长模型是什么?
AARRR模型(海盗指标)是增长黑客领域最经典的分析框架,将用户生命周期分为五个阶段,每个阶段有不同的核心指标和优化重点。
五个阶段:
1. Acquisition - 获取用户
核心目标:吸引潜在用户访问产品
关键指标:
- 新增用户数
- 各渠道流量
- 获客成本(CAC)
- 注册转化率
分析重点:
- 渠道效果对比(质量vs成本)
- 落地页转化率
- 获客ROI
2. Activation - 激活用户
核心目标:让用户体验到产品核心价值(Aha Moment)
关键指标:
- 激活率
- 新手引导完成率
- 核心功能使用率
- 首日行为深度
分析重点:
- 定义激活标准(如:发布第一条内容、添加第一个好友)
- 优化新手引导流程
- 缩短Aha Moment的时间
3. Retention - 留存用户
核心目标:让用户持续回访使用产品
关键指标:
- 次日留存率
- 7日留存率
- 30日留存率
- 月活/日活
分析重点:
- 留存曲线分析
- Magic Number识别
- 流失原因分析
- 召回策略
4. Revenue - 获取收入
核心目标:将用户转化为付费用户,产生收入
关键指标:
- 付费率
- ARPU(人均收入)
- ARPPU(付费用户人均收入)
- LTV(用户生命周期价值)
分析重点:
- 付费转化漏斗
- 定价策略
- 付费用户画像
- 提升客单价
5. Referral - 推荐传播
核心目标:用户自发推荐,形成病毒式增长
关键指标:
- K因子(病毒系数)
- 分享率
- NPS(净推荐值)
- 邀请转化率
分析重点:
- 分享动机设计
- 分享路径优化
- 邀请奖励机制
AARRR应用策略:
不同阶段产品的AARRR重点
早期产品(PMF验证期):
- 重点:Activation + Retention
- 目标:验证产品价值,找到核心用户
- 策略:小范围测试,快速迭代
成长期产品:
- 重点:Acquisition + Retention
- 目标:扩大用户规模,保持用户粘性
- 策略:优化获客渠道,提升留存率
成熟期产品:
- 重点:Revenue + Referral
- 目标:商业变现,自增长
- 策略:提升付费率,建立传播机制
AARRR与北极星指标:
在AARRR框架下,需要确定一个北极星指标(North Star Metric):
- 工具类产品:日活跃用户数(DAU)
- 内容类产品:内容消费时长
- 电商类产品:GMV或订单数
- 社交类产品:互动次数
北极星指标应该:
- 反映用户获得的核心价值
- 能够预测长期商业成功
- 可被团队直接影响
▶ 用户生命周期价值(LTV)如何计算?
LTV(Lifetime Value,用户生命周期价值)是指一个用户在整个生命周期内为产品带来的总收入,是评估用户价值和指导获客投入的核心指标。
基础计算公式:
LTV = ARPU × 平均生命周期月数
其中:
- ARPU = 总收入 / 总用户数(人均收入)
- 平均生命周期 = 1 / 月流失率
详细计算方法:
简化计算法
适用于稳定业务,快速估算
LTV = ARPU × (1 / 月流失率)
示例:
- ARPU = 50元/月
- 月流失率 = 20%
- LTV = 50 × (1/0.2) = 250元
优点:计算简单 缺点:忽略了时间价值和复杂因素
留存曲线法
基于实际留存数据计算
LTV = Σ(第N月ARPU × 第N月留存率)
示例(计算前12个月):
月份 ARPU 留存率 贡献
M1 50 100% 50
M2 50 60% 30
M3 50 40% 20
M4 50 30% 15
...
LTV = 50+30+20+15+... = 约200元
优点:更准确,考虑留存衰减 缺点:需要足够的历史数据
Cohort分析法
按注册时间分组,追踪真实LTV
步骤:
- 选择某个注册cohort(如2024年1月注册用户)
- 追踪该组用户每月的收入贡献
- 累加至今的总收入
- 除以该组用户数
优点:最准确,反映真实情况 缺点:需要长期追踪,早期产品数据不足
LTV与CAC的关系:
CAC(Customer Acquisition Cost)是获客成本,LTV/CAC是评估增长健康度的关键指标:
- LTV/CAC > 3:健康,获客有利润空间
- LTV/CAC = 1-3:需要优化,利润空间不足
- LTV/CAC < 1:亏损,不可持续
提升LTV的策略:
提升ARPU
- 提高客单价
- 增加购买频次
- 交叉销售/向上销售
延长生命周期
- 提升留存率
- 降低流失率
- 会员体系绑定
细分用户群
- 识别高LTV用户特征
- 针对性获客
- 差异化运营
实际应用:
电商平台案例:
- 计算不同渠道用户的LTV
- 对比渠道的CAC
- 发现:搜索广告LTV=300,CAC=80,ROI=3.75
- 发现:社交广告LTV=150,CAC=30,ROI=5
- 策略:虽然搜索用户LTV更高,但社交渠道ROI更好,可加大投入
LTV计算的注意事项
- 时间窗口选择:计算前12个月还是前24个月的LTV?
- 折现率考虑:长期收入需要考虑货币时间价值
- 用户分层:不同用户群体的LTV差异很大,不要只看平均值
- 动态调整:产品迭代会影响LTV,需要定期更新
- 成本归因:CAC应该包含哪些成本?(渠道费用、营销费用、人力成本?)
▶ 常见的电商核心指标有哪些?
电商数据分析涉及多个维度的指标,每个指标反映业务的不同侧面。
GMV相关指标:
GMV(Gross Merchandise Volume):成交总额,包含未支付订单
- 计算:GMV = 订单数 × 平均订单金额
- 反映平台整体规模
实际成交额:已支付订单的总金额
- 更真实反映业务收入
客单价(AOV - Average Order Value):平均每个订单的金额
- 计算:AOV = 总成交额 / 订单数
- 反映用户消费能力
流量指标:
- UV(Unique Visitor):独立访客数
- PV(Page View):页面浏览量
- PV/UV:人均浏览页面数,反映用户活跃度
转化指标:
- 整体转化率:下单用户数 / 访问用户数
- 加购转化率:加购用户数 / 访问用户数
- 支付转化率:支付用户数 / 下单用户数
用户指标:
- 新客/老客占比
- 复购率:再次购买的用户占比
- 购买频次:平均每个用户的购买次数
商品指标:
- SKU数:商品种类数
- 动销率:有销售的SKU占比
- 库存周转率:商品销售速度
运营效率指标:
- 退货率:退货订单数 / 总订单数
- 物流时效:发货到签收的平均时长
- 客服响应时间
A/B测试
是什么
请描述一下什么是A/B-test?
▶ 请描述一下什么是A/B-test?
定义类回答技巧:专业定义 + 场景结合
统计学定义:
- A/B测试是一种基于统计学的实验方法
- 通过设置对照组和实验组对变量进行试验
- 然后通过假设检验对不同组的结果进行检验
- 判断变量是否对最终结果造成显著影响
- 从而帮助选取最合理的方法
业务场景应用:
- AB测试是为同一目标制定不同页面版本
- 将用户流量分成对应组别
- 在同一时间维度让不同组用户随机访问这些版本
- 通过埋点设计收集各群组的用户体验和业务数据
- 分析评估出最优版本并正式采用
A/B测试的核心原理是什么?
▶ A/B测试的核心原理是什么?
A/B测试本质就是一个基于统计的假设检验过程
- 通过随机合理分流,设置对照组和实验组
- 通过控制变量法,观察两组用户在一段时间内的表现
- 通过假设检验,分析结果是否有显著差异从而判断改动是否有效可执行
其他理论:
- 中心极限定理:样本量足够大时,变量均值的抽样分布都近似于正态分布
- 小概率事件:小概率事件在一次实验中基本上不会发生
- 反证法:假设检验的机制是保护原假设,所以把要拒绝的假设放在原假设的位置,如果原假设成立的条件下,小概率事件还是发生了,那么就应该推翻原假设
- P值:P值越小,拒绝原假设的理由就越充分
A/B测试有哪些应用场景?
▶ AB测试有哪些应用场景?
AB测试适用的场景
- 产品迭代:比如UI界面优化、产品功能增加或者改版、流程增加或者删除等
- 算法优化:比如搜索、推荐、精准广告等算法的优化
- 营销/运营策略优化:比如内容的筛选、时间的筛选、人群的筛选、运营玩法的筛选等
AB测试不适用的场景
- 原始创新方案且全量投放:比如公司更新logo,这种要给所有用户看的场景不能做AB测试
- 用户体量不大的业务:主要是样本量不够,难以支撑A/B测试所需样本量
有没有接触过A/B-test,请说说你对A/B测试的理解
▶ 有没有接触过AB-test,请说说你对AB测试的理解
A/B测试流程:
- 确定实验目标和假设
- 确定观测指标
- 样本量的计算
- 流量分割
- 实验周期的计算
- 实施测试
- 灰度
- 埋点设计
- 效果评估验证
如何配合 遇到什么难题 如何解决
对AB测试的理解:
你怎么理解A/B测试中的第一、二类错误?
▶ 你怎么理解A/B测试中的第一、二类错误?
第一类错误(弃真错误): 原假设为真时错误地拒绝原假设
- 实际业务场景:功能改动实际无差异(为真),但误认为有显著收益而上线该功能
第二类错误(存伪错误): 原假设为假时错误地接受原假设
- 实际业务场景:功能改动实际有效是好产品(为假),但误判为无效而放弃上线
在实际工作中,我认为第一类错误是更加不能接受的:
因为,在商业世界里,一个坏产品的上线带来的损失可能是巨大的,所以我们宁愿放弃几个好的创意好的产品,也绝不能让坏的产品上线。如果一个坏产品上线了,不仅可能会极大程度的影响当时的用户体验,还有可能对以后的日活、留存等指标造成更大的影响。本身在实际工作中,把留存或者日活提升一个百分点都已经是非常耗时耗力的事情了,一个坏产品的上线可能一瞬间就能让留存下降好几个百分点。
为什么
为什么要做A/B测试?A/B测试有什么好处?有什么科学依据?
▶ 为什么要做A/B测试?A/B测试有什么好处?有什么科学依据?
回答要点:A/B测试的目的/好处+理论基础
A/B测试的目的/好处: 首先我认为
- 功能设计者是有个人思维的局限性
- 全量用户具有不可调研性 这就会导致一个问题一个功能的预期效果可能与实际上线后的效果存在一定的差异,这个差异到底有多大、我们能不能接受,最后要不要上线这个功能,这些都是需要进行决断的
理论基础: AB测试是一个基于统计的假设检验过程,首先对实验组和对照组的关系提出假设,然后计算两组数据的差异并确定该差异是否存在统计上的显著性,最后依据数据结果对假设做出判断
这个过程可以很大程度上避免我们拍脑袋决策,科学量化优化方案的效果
A/B测试成本很高,每个调整都需要做A/B测试吗?
▶ A/B测试成本很高,每个调整都需要做A/B测试吗?
回答要点:说出你理解的AB测试发起的条件,比如改动的影响程度、收益
需要从源头了解功能改动的重要性、影响程度等:
如果重要性或者影响程度很大,那么是一定要做AB测试的 比如一个大型营销活动的落地页设计,就非常值得做AB测试,首先是这个页面的改动有可能直接影响最后的销售额,因为这个页面很大程度决定了用户是否对你的产品感兴趣,并为此而付费
此外,一个大型营销活动一定是投入了大量的人力物力财力,每一个细节都有可能决定最终的结果,所以前期一定要尽可能筛出最优的方案
如果只是验证一个小按钮或者一个小改动,并且这个改动并不会对用户体验、活动结果、最终收益产生巨大影响的时候,就可以选择不上AB测试了 比如在界面上按设置一个开关,用户可以通过开关的形式自行决定采用哪一种方式,最后我们可以通过这个开关的相关指标来判断用户对于哪一种形式有更大的倾向性或者可以做一些用户调研、比如通过访谈或者说设计问卷的形式,去搜集一些用户的反馈等等
怎么做
AB测试的主要流程是什么?
▶ A/B测试的主要流程是什么?
回答要点:A/B测试的主要流程+业务场景
A/B测试的主要流程:
- 确定实验目标和假设:首先需要和相关的产品或者项目经理确定这个实验所要验证的改动点是什么
- 确定观测指标:数据分析师需要设计实验中所需要去观测的一些核心指标,比如点击率、转化率等
- 样本量的计算:计算实验所需的最少样本量,由于实验样本越大结果越可信但是对用户的不良影响就越大,所以需要计算能够显著地证明策略有效的最少样本量
- 流量分割:设计流量分割策略,根据实验需要对样本流量进行分流分层,保证样本的随机和均匀分布,避免出现辛普森悖论
- 实验周期的计算:结合目前的日均活跃的用户量,计算实验持续的时间周期
- 实施测试:和产品经理、开发人员确认可以开始实验
- 灰度测试:目的就是为了验证这个改动并不会造成特别极端的影响
- 埋点设计:检验数据埋点是否跑通,是否能够成功搜集到数据
- 效果评估验证:对实验的结果进行显著性检验以及最终的效果评估验证,实验结果主要分成有效和无效两种
- 有效的结果:即成功通过实验提升了产品的转化率,可以把优胜的版本正式推送给全部客户,实现产品用户有效增长
- 无效的结果:可转化为团队的经验,避免再犯同样的错误
选择A/B实验的样本时,需要注意什么
▶ 选择AB实验的样本时,需要注意什么?
回答要点:A/B测试的主要流程+业务场景
选择A/B实验样本的注意事项:
- 满足最小样本量:样本量既不能太少(影响可信度)也不能太多(影响用户体验)
影响样本量选择的四个因素:
- 显著性水平($α$):显著性水平越低,对AB测试结果的要求也就越高,越需要更大的样本量来确保精度
- 统计功效($1-β$):统计功效意味着避免犯二类错误的概率,统计功效越大,需要的样本量也越大
- 均值差异($δ$):如果真实值和测试值的均值差别巨大,也不太需要多少样本,就能达到统计显著
- 标准差($σ$):标准差越小,代表两组差异的趋势越稳定,越容易观测到显著的统计结果
每组所需最小样本量: $$ N=\frac{\sigma^2}{\delta^2}(Z_{1-\frac{\alpha}{2}}+Z_{1-\beta})^2 $$
实际业务中在不影响用户体验的前提下,可以比最小样本数多一些
- 时间一致:必须选择同一时间段内的用户,避免时间因素干扰
- 特征一致:确保实验组和对照组用户特征分布相似
- 随机化:可通过随机抽样实现样本代表性
介绍一下A/B测试,以及所需样本量计算公式是什么?
▶ 介绍一下A/B测试,以及所需样本量计算公式是什么?
回答要点:A/B测试的理解+公式
A/B测试的理解: AB测试是一种基于统计学的实验方法,通过设置对照组和实验组,对变量进行试验,通过假设检验对不同组的结果进行检验,以检验变量是否对结果造成显著影响,从而选取最合理的方法
在产品迭代、算法迭代、运营策略优化等场景下,A/B测试能够帮助我们更加科学地进行决策
每组所需最小样本量计算公式:
$$ N=\frac{\sigma^2}{\delta^2}(Z_{1-\frac{\alpha}{2}}+Z_{1-\beta})^2 $$
- 显著性水平$α$为犯第一类错误的概率
- $β$为犯第二类错误的概率
- $σ$代表的是样本数据的标准差
- 比率值:$\sigma^{2} = P_{A}(1 - P_{A}) + P_{B}(1 - P_{B})$,$P_{A}$、$P_{B}$分别是对照组和实验组的观测值
- 绝对值:$\sigma^2=\frac{\sum_1^n(x_i-\bar{x})^2}{n-1}$, $\bar{x}$指的是样本均值,$n$为样本数
- $δ$代表的是预期实验组和对照组两组数据的差值
A/B测试的实验周期如何选择?需要考虑哪些因素?过长或过短有什么影响?
▶ A/B测试的实验周期如何选择?需要考虑哪些因素?过长或过短有什么影响?
实验所需最少周期计算公式:某时间段可以是每天,流量大可以按每小时
$$ 实验所需最少周期=\frac{\text{每组最小样本数}\times\text{组数量}}{\text{某时间段的访问流量}}$$
实验周期选择的因素:
- 最小样本量:实验周期内累计样本量必须大于最小样本量要求
- 周末效应:周中(工作日)和周末用户行为存在显著差异,实验周期至少需要运行完整的7天
- 新奇效应:重点针对老用户对改版会产生的非持久性行为,这段时间置信度较低,需适当拉长试验周期
实验周期选择的影响:
- 过长:导致实验迭代的效率变低
- 过短:导致用户体验受影响,实验不置信
如何进行合理的流量分割?
▶ 如何进行合理的流量分割?
流量分割方法:
分流(用户互斥):按地域/性别/年龄等将用户均匀分组,一个用户仅出现在一个组
组间互斥(组1+组2=100%流量)
分层(用户正交):同一批流量可分布在多个无业务关联的实验层
每一层用完的流量进入下一层时,一定均匀的重新分配
分流分层模型:分流+分层
- 分流阶段:将总流量分为互斥的组1和组2(各50%)
- 分层阶段:组2流量可复用至B1/B2/B3层
- 扩展应用:B1层可再分为互斥的B1-1/B1-2/B1-3
如何验证你的改进办法有效果?如何确定此功能上线收益?
▶ 如何验证你的改进办法有效果?如何确定此功能上线收益?
通过假设检验对关键性指标进行检验:比如:点击率、留存率、复购率、转化率、人均时长等(样本均值的 t 检验和样本比例的 z 检验)
- 结论置信:得到A/B case哪个指标更好(有显著性差异)
- 结论置信:进一步找问题
计算每组投入产出比($\text{ROI} = \frac{\text{收益} - \text{成本}}{\text{成本}}$)对比哪组case有效:
- 成本:每个实验组成本可以直接计算
- 收益:和对照组相比较,假定以DAU作为收益指标,需要假设不做运营活动:
- $\text{实验组DAU}_\text{假设不做活动} = 对照组DAU \times \frac{\text{实验组流量}}{\text{对照组流量}}$
- $\text{实验组收益} = \text{实验组DAU} - \text{实验组DAU}_\text{假设不做活动}$
- 一般有活动相比无活动,留存、人均时长等各项指标均会更显著
请分析下A/B-test的结果统计显著不等于实际显著,你怎么看?
▶ 请分析下A/B-test的结果统计显著不等于实际显著,你怎么看?
- 统计学上显著,实际不显著:可能选取的样本量过大,导致和总体数据量差异很小 举例:对应到我们的互联网产品实践当中,我们做了一个改动,APP的启动时间的优化了0.001秒,这个数字可能在统计学上对应的P值很小(统计学上是显著的),但是在实际使用过程中,对于用户来说,0.001秒的差异太微小了,是感知不出来的。那么这样一个显著的统计差别,其实对我们来说是没有太大的实际意义的
- 统计学上不显著,实际显著:一般我们处理通用的方式是将这个指标去拆分成每天去观察 如果指标的变化曲线每天实验组都高于对照组,即使它在统计学上说是不显著的,我也认为在这样一个观测周期内,实验组的关键指标表现优于对照组的,那么结合这样一个观测,我们最终也可以得出这个优化可以上线的结论
若在A/B测试中发现实验组核心指标明显优于对照组,那这个优化就一定能够上线吗?
▶ 若在A/B测试中发现实验组核心指标明显优于对照组,那这个优化就一定能够上线吗?
不一定,需要实际情况实际分析
- 举例:提升产品视觉展现效果的UI优化,代价是增加用户等待内容展现的时间
- 影响:可能导致用户耐心下降,对其他部门产生负向影响,最终可能造成公司整体收入下降
- 总结:需要全面评估所有相关指标的变动,同时评估收益和损失,才可以确认这个优化可以上线
场景题
美团打算从外卖的主营业务拓展到其他业务中,比如美团跑腿。公司考虑向用户发送APP内的推送通知来推广这一个新业务。你会如何设计和分析一个A/B测试来决定是否应该推出这个推送通知?
▶ 美团打算从外卖的主营业务拓展到其他业务中,比如美团跑腿。公司考虑向用户发送APP内的推送通知来推广这一个新业务。你会如何设计和分析一个A/B测试来决定是否应该推出这个推送通知?
询问问题细节,与面试官产生互动,以更好地理解业务目标和产品特性细节
- 我:我想先确认一下我对问题背景,类似这样的功能可能会有多个不同的业务目标——比如增加新用户获取、增加这项业务的转化率、增加该业务订单数量或增加总订单销售额
- 面试官:通过App推送通知,我们主要是想提高这项新业务的转化率——即在所有登录或活跃用户中,该新业务下单用户的占比
- 我:好的,那现在我可以了解更多关于通知的内容么?比如推送的信息是什么?目标受众是谁?
- 面试官:目前我们不提供任何折扣,我们只是想让他们知道,我们有了一项新的业务,他们可以开始使用,如果实验成功,我们打算向所有用户推广出通知
- 我:好的,谢谢介绍,现在,我准备深入研究实验的细节
陈述业务假设和定义要评估的指标
除了主要指标之外,还要考虑次要指标和护栏指标- 我:我们的期望是,如果我们发送App推送通知,那么新业务的每日订单数量将会增加,转化率也会增加,所以,原假设$H_0$:发送App通知,转化率没有任何变化
- 我:
- 主要指标:转化率,因为通知的目标是提高新业务的转化率
- 次要指标:平均订单价值,因为转化率可能会上升,但如果平均订单价值下降、从而导致整体收益下降的话,那这也不是我们希望看到的
- 护栏指标:产品的性能指标,不希望我们做的这个A/B测试影响到它们
- 面试官:我同意你对主要指标的选择,但当前的场景下,你可以忽略次要指标,在护栏指标方面,你是正确的——当涉及到App时,美团希望对任何功能或发行版本保持谨慎态度,因为我们知道安装了App的用户的生命周期价值(LTV)要高得多,我们要尽量避免用户卸载App
- 我:好的,那当前我们就可以把卸载的百分比作为我们的护栏指标
护栏指标是用来帮助我们戒备成功指标给予错误信号的情况,不直接表示实验组是否成功,但是从提供另一种维度来描述实验而提供了更全面的分析
- 可以是产品的性能指标:比如,多少搜索成功完成,平均耗时多少?虽然这些度量并不完全决定是否发布新的搜索引擎,但是如果我们发现它的表现非常差,即使成功指标(相关性)有些许的提高,我们往往也不会发布新的产品
- 也可以是产品不直接影响的商业价值指标:在做用户增长实验时,也可以将用户体验作为护栏指标,虽然大部分的新产品和新功能都不应该影响用户体验,但是将它们加入护栏指标可以对实验结果更有信心
选择显著性水平、统计功效,计算所需的样本大小和实验周期
对统计概念和最小样本量的计算以及实验周期计算的掌握程度,你是否考虑了各类因素对实验有效性的影响,比如:网络效应经常影响双边市场类的公司或社交网络、工作日效应、季节性/周期性或新奇效应- 网络效应:经济学用语,指的是某种产品对一名用户的价值取决于使用该产品的其他用户的数量
- 工作日效应:周中和周末的用户行为表现存在着一定的差异
- 新奇效应:一个新事物/新产品/新功能,在最初被用户熟知的时候会有提高的趋势(相对于平稳之后)
- 我:定义好原假设以及后续要评估的指标之后,就可以正式进入实验设计阶段了,在当前场景中是否需要考虑网络效应呢?考虑网络效应,我们将需要选择不同于我们通常所做的随机单位,比如基于地理的随机化、基于时间的随机化、网络集群的随机化或以网络自我为中心的随机化等等
- 面试官:为了节省时间,这里假设不存在网络效应,继续
- 我:假设没有网络效应,那么实验的随机单位就是用户,我们将随机选择用户,并将他们分配到实验和控制组中,实验组将收到通知,控制组将不会收到任何通知,接下来,需要计算样本大小和实验持续时间,为此我需要输入一些指标:
- 基线转化:这是在进行更改之前对照组已经存在的转化
- 最小可检测差异:在实施和维护功能成本合理的情况下,实现理想结果所需要的的提升
- 显著性水平:当原假设成立时,拒绝原假设的概率
- 统计功效:即测试正确拒绝原假设的概率(避免犯第二类错误的概率)
- 我:通常会选择5%的显著性水平和80%的统计功效,再加上基线转化和期望提升的转化,带入到最小样本量计算公式中就可以计算出每一组所需要的最小样本量了,最小样本量计算公式如下: $$N = \frac{\sigma^2}{\delta^2}(Z_{1-\frac{\alpha}{2}} + Z_{1-\beta})^2 $$
-显著性水平$α$为犯第一类错误的概率 - $β$为犯第二类错误的概率 - $σ$代表的是样本数据的标准差 - 比率值:$\sigma^{2} = P_{A}(1 - P_{A}) + P_{B}(1 - P_{B})$,$P_{A}$、$P_{B}$分别是对照组和实验组的观测值 - 绝对值:$\sigma^2=\frac{\sum_1^n(x_i-\bar{x})^2}{n-1}$, $\bar{x}$指的是样本均值,$n$为样本数 - $δ$代表的是预期实验组和对照组两组数据的差值
- 面试官:好的,现在加入通过分析我们知道了每个组需要10000个用户的样本大小,你会怎么计算测试的持续时间?
- 我:我们需要知道每天登录到APP的用户数量
- 面试官:假设我们每天有1万名用户登录这个App
- 我:在这种情况下,我们至少需要2天来进行实验——我是通过将控制组和实验组的总样本量除以每日用户数量得出这个结论的,但是,在确定期限时,我们还应该考虑其他因素,比如:
- 工作日效应:可能会在周末和工作日有不同的用户群,因此运行足够长的时间来捕捉每周周期是很重要的
- 季节性效应:需要考虑到的是,有些时间用户行为可能不同,比如节假日
- 新奇效应:当你引入一个新功能,特别是一个很容易被注意到的功能时,一开始它会吸引用户去尝试它。所以实验可能在一开始表现得很好,但随着时间的推移,效果会迅速下降。
- 外部效应:例如,假设市场运行得很好,更多的人可能会因为期望获得高回报而忽略通知,这将使我们从实验中得出错误的结论
- 综上所述,我建议实验至少进行一周
- 面试官:好的,听起来还算合理,你如何分析测试结果?
分析结果并得出结论:在不同情况下使用的适当统计检验的知识(例如,样本均值的t检验和样本比例的z检验);考虑随机性(这会给你加一些印象分);提供最终的建议(或实现该建议的框架)
- 我:
- 考虑随机性:应该在分配实验组和对照组时,考虑随机性是否正确,可以查看一些不希望受到测试影响的基线指标,并通过比较这些指标在两组之间的直方图或密度曲线来进行对比,如果没有区别,我们可以得出结论,随机性是正确的
- 所有指标(包括主要指标和护栏指标)的显著性检验:主要指标(转化率)和护栏指标(卸载率)都是比率,可以用z检验来检验统计的显著性(python)
- 最终建议:
- 转化率有了显著的增长,卸载率也没有受到负面影响,我建议执行测试
- 转化率有了显著的增长,卸载率受到了负面影响,我建议不要执行测试
- 转化率在统计意义上没有显著的提高,我建议不要执行测试
Vue 基础
▶ Vue 2 和 Vue 3 有哪些主要区别?
主要区别包括:
性能提升:
- Vue 3 使用 Proxy 代替 Object.defineProperty,性能更好
- 编译器优化,静态提升,事件缓存
- Tree-shaking 支持更好,打包体积更小
Composition API:
- Vue 3 引入 Composition API,提供更好的逻辑复用
- setup 函数替代选项式 API
其他特性:
- 支持多个根节点(Fragments)
- Teleport 组件
- Suspense 组件
- 更好的 TypeScript 支持
▶ 什么是响应式原理?Vue 2 和 Vue 3 的实现有何不同?
Vue 2 响应式原理:
- 使用
Object.defineProperty劫持对象属性的 getter/setter - 通过依赖收集和派发更新实现响应式
- 局限性:无法检测对象属性的添加/删除,数组索引和长度的变化
Vue 3 响应式原理:
- 使用
Proxy代理整个对象 - 可以监听对象属性的添加/删除,数组的变化
- 性能更好,支持 Map、Set 等数据结构
// Vue 2
Object.defineProperty(obj, 'key', {
get() { /* 收集依赖 */ },
set() { /* 触发更新 */ }
});
// Vue 3
new Proxy(obj, {
get(target, key) { /* 收集依赖 */ },
set(target, key, value) { /* 触发更新 */ }
});
▶ v-if 和 v-show 的区别?什么时候使用哪个?
v-if:条件渲染,会销毁和重建 DOM 元素,有更高的切换开销v-show:只是切换 CSSdisplay属性,有更高的初始渲染开销
使用场景:
- 频繁切换:使用
v-show - 条件很少改变:使用
v-if - 涉及权限控制:使用
v-if(不渲染 DOM)
▶ computed 和 watch 的区别?
computed(计算属性):
- 基于依赖缓存,只有依赖变化才会重新计算
- 必须有返回值
- 适合:复杂的数据计算、过滤、排序
watch(侦听器):
- 监听数据变化执行回调
- 不需要返回值
- 适合:异步操作、开销较大的操作、监听 props 变化
// computed
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
// watch
watch: {
firstName(newVal) {
this.fetchUserData(newVal); // 异步操作
}
}
▶ Vue 的 key 属性有什么作用?
key 是 Vue 用于识别 VNode 的唯一标识,在 diff 算法中起到关键作用。
作用:
- 帮助 Vue 追踪节点的身份,优化更新性能
- 重用和重新排序现有元素
- 强制替换元素而不是复用
注意事项:
- 不要使用数组索引作为 key(会导致性能问题和状态错误)
- key 应该是稳定、唯一的标识符
- 使用 v-for 时必须提供 key
组件通信
▶ Vue 组件间通信有哪些方式?
- Props / Emit:父子组件通信
- Provide / Inject:祖先组件向后代组件传递数据
- Event Bus:任意组件间通信(Vue 3 推荐使用 mitt 库)
- Vuex / Pinia:全局状态管理
- $attrs / $listeners:透传属性和事件
- $parent / $children:直接访问父子组件实例(不推荐)
- ref:获取组件实例
▶ 如何实现父子组件双向绑定?
使用 v-model 或 .sync 修饰符(Vue 2)/ v-model:propName(Vue 3)。
<!-- 父组件 -->
<ChildComponent v-model="value" />
<!-- 等同于 -->
<ChildComponent :modelValue="value" @update:modelValue="value = $event" />
<!-- 子组件 -->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
updateValue(newValue) {
this.$emit('update:modelValue', newValue);
}
}
}
</script>
▶ Provide / Inject 的使用场景?有什么限制?
使用场景:
- 深层嵌套组件间传递数据
- 组件库开发(如 Form 和 FormItem 通信)
- 主题配置、国际化等全局配置
限制:
- 不是响应式的(Vue 2),需要提供响应式对象
- Vue 3 中默认是响应式的
- 不能用于非祖先后代关系的组件
// 祖先组件
provide() {
return {
theme: this.theme,
updateTheme: this.updateTheme
}
}
// 后代组件
inject: ['theme', 'updateTheme']
生命周期
▶ Vue 生命周期钩子有哪些?各自的作用是什么?
创建阶段:
beforeCreate:实例初始化后,data、methods 未初始化created:实例创建完成,可访问 data、methods,但未挂载 DOM
挂载阶段:
beforeMount:模板编译完成,即将挂载mounted:DOM 挂载完成,可操作 DOM
更新阶段:
beforeUpdate:数据更新,DOM 更新前updated:DOM 更新完成
销毁阶段:
beforeUnmount(Vue 3)/beforeDestroy(Vue 2):实例销毁前unmounted(Vue 3)/destroyed(Vue 2):实例销毁完成
常用场景:
created:调用 API 获取数据mounted:操作 DOM、初始化第三方库beforeUnmount:清理定时器、取消订阅
▶ 父子组件生命周期执行顺序是怎样的?
加载渲染过程:
- 父 beforeCreate
- 父 created
- 父 beforeMount
- 子 beforeCreate
- 子 created
- 子 beforeMount
- 子 mounted
- 父 mounted
更新过程:
- 父 beforeUpdate
- 子 beforeUpdate
- 子 updated
- 父 updated
销毁过程:
- 父 beforeUnmount
- 子 beforeUnmount
- 子 unmounted
- 父 unmounted
Vue Router
▶ Vue Router 的路由模式有哪些?区别是什么?
1. Hash 模式:
- URL 带
#,如http://example.com/#/user - 利用 hashchange 事件监听
- 兼容性好,不需要服务器配置
2. History 模式:
- URL 正常,如
http://example.com/user - 利用 HTML5 History API
- 需要服务器配置,刷新时返回 index.html
3. Memory 模式:
- 不依赖浏览器 URL
- 用于 SSR 或非浏览器环境
const router = createRouter({
history: createWebHistory(), // History 模式
// history: createWebHashHistory(), // Hash 模式
// history: createMemoryHistory(), // Memory 模式
routes
});
▶ 路由守卫有哪些?如何实现登录验证?
全局守卫:
beforeEach:全局前置守卫beforeResolve:全局解析守卫afterEach:全局后置钩子
路由独享守卫:
beforeEnter
组件内守卫:
beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave
登录验证示例:
router.beforeEach((to, from, next) => {
const isAuthenticated = localStorage.getItem('token');
if (to.meta.requiresAuth && !isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } });
} else {
next();
}
});
// 路由配置
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
}
▶ 如何实现路由懒加载?为什么要懒加载?
为什么要懒加载:
- 减少首屏加载时间
- 按需加载,提升性能
- 减小打包体积
实现方式:
// 1. 动态 import
const Home = () => import('./views/Home.vue');
// 2. webpack 魔法注释
const About = () => import(/* webpackChunkName: "about" */ './views/About.vue');
// 3. 分组打包
const UserProfile = () => import(/* webpackChunkName: "user" */ './views/UserProfile.vue');
const UserPosts = () => import(/* webpackChunkName: "user" */ './views/UserPosts.vue');
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/user/profile', component: UserProfile },
{ path: '/user/posts', component: UserPosts }
];
Vuex / Pinia
▶ Vuex 和 Pinia 的区别?为什么 Vue 3 推荐使用 Pinia?
Pinia 优势:
- 更简洁的 API,去除了 mutations
- 完整的 TypeScript 支持
- 模块化更简单,不需要嵌套
- 支持多个 store 实例
- 更好的 DevTools 支持
- 体积更小
对比:
// Vuex
export default new Vuex.Store({
state: { count: 0 },
mutations: {
increment(state) { state.count++; }
},
actions: {
increment({ commit }) { commit('increment'); }
}
});
// Pinia
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() { this.count++; }
}
});
▶ 如何在 Pinia 中处理异步操作?
在 Pinia 中,actions 可以直接是异步函数:
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
loading: false,
error: null
}),
actions: {
async fetchUser(id) {
this.loading = true;
this.error = null;
try {
const response = await fetch(`/api/users/${id}`);
this.user = await response.json();
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
},
async updateUser(userData) {
try {
const response = await fetch(`/api/users/${this.user.id}`, {
method: 'PUT',
body: JSON.stringify(userData)
});
this.user = await response.json();
} catch (error) {
console.error('Update failed:', error);
throw error;
}
}
}
});
Composition API
▶ Composition API 相比 Options API 有什么优势?
优势:
- 更好的逻辑复用:通过 composables 函数
- 更好的类型推导:完整 TypeScript 支持
- 更灵活的代码组织:按功能而非选项组织
- 更好的 Tree-shaking:未使用的功能不会打包
示例:
// Composable 函数
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(e) {
x.value = e.pageX;
y.value = e.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
// 在组件中使用
const { x, y } = useMouse();
▶ ref 和 reactive 的区别?什么时候用哪个?
ref:
- 用于基本类型和对象类型
- 访问值需要
.value - 适合:基本类型、单个值
reactive:
- 只能用于对象类型
- 直接访问属性,无需
.value - 适合:复杂对象、表单数据
// ref
const count = ref(0);
count.value++; // 需要 .value
// reactive
const state = reactive({
count: 0,
name: 'John'
});
state.count++; // 直接访问
// 注意:reactive 解构会失去响应性
const { count } = state; // ❌ 失去响应性
const { count } = toRefs(state); // ✅ 保持响应性
▶ watchEffect 和 watch 的区别?
watchEffect:
- 立即执行,自动追踪依赖
- 不需要指定监听源
- 适合:副作用操作
watch:
- 惰性执行(除非设置 immediate: true)
- 需要明确指定监听源
- 可以访问新旧值
- 适合:需要对比新旧值、条件执行
// watchEffect
watchEffect(() => {
console.log(`Count is: ${count.value}`);
// 自动追踪 count 的变化
});
// watch
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
});
// watch 多个源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
// ...
});
性能优化
▶ Vue 性能优化有哪些手段?
1. 代码层面:
- 使用
v-show代替v-if(频繁切换) - 使用
computed缓存计算结果 - 使用
keep-alive缓存组件 - 使用虚拟滚动处理长列表
- 避免在模板中使用复杂表达式
2. 打包优化:
- 路由懒加载
- 组件按需引入
- Tree-shaking
- 代码分割
3. 渲染优化:
- 使用
Object.freeze()冻结不需要响应式的数据 - 合理使用
key属性 - 避免不必要的组件抽象
4. 网络优化:
- 使用 CDN
- 开启 Gzip 压缩
- 图片懒加载
- 接口防抖节流
// 冻结数据
data() {
return {
list: Object.freeze(largeArray)
}
}
// 虚拟滚动
<virtual-scroller :items="items" :item-height="50" />
▶ 如何优化大型列表渲染?
1. 虚拟滚动(Virtual Scroll):
- 只渲染可视区域的元素
- 使用
vue-virtual-scroller或vue-virtual-scroll-list
2. 分页加载:
- 每次只加载一部分数据
- 无限滚动 + 懒加载
3. 使用 Object.freeze():
- 冻结不需要响应式的数据
4. 合理使用 key:
- 使用唯一标识而非索引
示例:
<template>
<virtual-scroller
:items="items"
:item-height="50"
class="scroller"
>
<template #default="{ item }">
<div class="item">{{ item.name }}</div>
</template>
</virtual-scroller>
</template>
<script setup>
import { ref } from 'vue';
const items = ref(
Object.freeze(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}))
)
);
</script>
▶ keep-alive 的作用和使用场景?
作用:
- 缓存不活跃的组件实例
- 避免重复渲染,保留组件状态
使用场景:
- 标签页切换
- 表单填写中途切换
- 列表详情页切换
属性:
include:匹配的组件会被缓存exclude:匹配的组件不会被缓存max:最多缓存多少组件实例
<!-- 缓存所有路由组件 -->
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
<!-- 只缓存特定组件 -->
<keep-alive :include="['ComponentA', 'ComponentB']">
<component :is="currentView" />
</keep-alive>
<!-- 生命周期钩子 -->
<script>
export default {
activated() {
console.log('组件被激活');
},
deactivated() {
console.log('组件被缓存');
}
}
</script>
实战场景题
▶ 如何实现一个数据看板系统?需要考虑哪些技术点?
一个完整的数据看板系统需要考虑以下技术点:
1. 架构设计:
├── api/ # API 封装
├── components/ # 公共组件
│ ├── charts/ # 图表组件
│ ├── filters/ # 筛选组件
│ └── layouts/ # 布局组件
├── composables/ # 可复用逻辑
├── stores/ # 状态管理
├── utils/ # 工具函数
└── views/ # 页面
2. 核心功能实现:
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useDataStore } from '@/stores/dataStore';
import * as echarts from 'echarts';
const dataStore = useDataStore();
// 数据采集
const fetchData = async () => {
await dataStore.fetchSalesData();
await dataStore.fetchUserStats();
await dataStore.fetchProductAnalysis();
};
// 数据处理
const processedData = computed(() => {
const data = dataStore.salesData;
return data.map(item => ({
...item,
avgOrderValue: item.sales / item.orders,
growthRate: calculateGrowth(item)
}));
});
// 图表初始化
const initCharts = () => {
const chart = echarts.init(document.getElementById('chart'));
chart.setOption({
xAxis: { type: 'category', data: dates },
yAxis: { type: 'value' },
series: [{ type: 'line', data: values }]
});
};
// 实时更新
const startAutoRefresh = () => {
setInterval(() => {
fetchData();
}, 30000); // 每30秒刷新
};
onMounted(() => {
fetchData();
initCharts();
startAutoRefresh();
});
</script>
3. 性能优化:
- 使用 Pinia 统一管理数据
- ECharts 按需引入
- 虚拟滚动处理大量数据
- 防抖节流处理筛选操作
- 使用 Web Worker 处理复杂计算
4. 交互设计:
- 筛选条件联动
- 图表联动(点击柱状图,折线图联动更新)
- 导出数据功能
- 响应式布局
5. 数据可视化:
- 折线图:展示趋势
- 柱状图:对比数据
- 饼图:展示占比
- 散点图:相关性分析
- 地图:地理分布
▶ 如何实现一个表单构建器(Form Builder)?
核心功能:
- 拖拽式表单设计
- 组件配置
- 表单验证
- 数据绑定
- 表单渲染
实现思路:
<!-- FormBuilder.vue -->
<template>
<div class="form-builder">
<!-- 组件库 -->
<div class="component-library">
<draggable v-model="components" :group="{ name: 'fields', pull: 'clone', put: false }">
<div v-for="comp in availableComponents" :key="comp.type" class="component-item">
{{ comp.label }}
</div>
</draggable>
</div>
<!-- 画布区域 -->
<div class="canvas">
<draggable v-model="formConfig.fields" group="fields" @change="handleFieldChange">
<div v-for="(field, index) in formConfig.fields" :key="field.id"
class="field-wrapper"
@click="selectField(field)">
<component :is="getFieldComponent(field.type)" v-bind="field" />
<button @click="removeField(index)">删除</button>
</div>
</draggable>
</div>
<!-- 属性配置 -->
<div class="properties">
<div v-if="selectedField">
<h3>字段配置</h3>
<el-form>
<el-form-item label="字段名">
<el-input v-model="selectedField.name" />
</el-form-item>
<el-form-item label="标签">
<el-input v-model="selectedField.label" />
</el-form-item>
<el-form-item label="必填">
<el-switch v-model="selectedField.required" />
</el-form-item>
<el-form-item label="验证规则">
<el-select v-model="selectedField.rules" multiple>
<el-option label="邮箱" value="email" />
<el-option label="手机号" value="phone" />
<el-option label="身份证" value="idCard" />
</el-select>
</el-form-item>
</el-form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import draggable from 'vuedraggable';
const formConfig = ref({
fields: []
});
const selectedField = ref(null);
const availableComponents = [
{ type: 'input', label: '输入框' },
{ type: 'textarea', label: '多行文本' },
{ type: 'select', label: '下拉选择' },
{ type: 'radio', label: '单选框' },
{ type: 'checkbox', label: '复选框' },
{ type: 'date', label: '日期选择' },
{ type: 'upload', label: '文件上传' }
];
const getFieldComponent = (type) => {
const componentMap = {
input: 'el-input',
textarea: 'el-input',
select: 'el-select',
radio: 'el-radio-group',
checkbox: 'el-checkbox-group',
date: 'el-date-picker',
upload: 'el-upload'
};
return componentMap[type];
};
const addField = (type) => {
formConfig.value.fields.push({
id: Date.now(),
type,
name: `field_${formConfig.value.fields.length}`,
label: '',
required: false,
rules: []
});
};
const removeField = (index) => {
formConfig.value.fields.splice(index, 1);
};
const selectField = (field) => {
selectedField.value = field;
};
// 导出配置
const exportConfig = () => {
return JSON.stringify(formConfig.value, null, 2);
};
// 导入配置
const importConfig = (config) => {
formConfig.value = JSON.parse(config);
};
</script>
表单渲染器:
<!-- FormRenderer.vue -->
<template>
<el-form :model="formData" :rules="formRules" ref="formRef">
<el-form-item
v-for="field in formConfig.fields"
:key="field.id"
:label="field.label"
:prop="field.name"
>
<component
:is="getFieldComponent(field.type)"
v-model="formData[field.name]"
v-bind="field.props"
/>
</el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
</el-form>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps({
formConfig: Object
});
const formRef = ref(null);
const formData = ref({});
// 生成验证规则
const formRules = computed(() => {
const rules = {};
props.formConfig.fields.forEach(field => {
if (field.required || field.rules.length > 0) {
rules[field.name] = [];
if (field.required) {
rules[field.name].push({
required: true,
message: `请输入${field.label}`,
trigger: 'blur'
});
}
field.rules.forEach(rule => {
rules[field.name].push(getRuleConfig(rule));
});
}
});
return rules;
});
const submitForm = async () => {
await formRef.value.validate();
console.log('提交数据:', formData.value);
};
</script>
▶ 如何实现一个权限管理系统?包括路由权限和按钮权限。
1. 路由权限:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { useUserStore } from '@/stores/user';
// 路由配置
const routes = [
{
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true,
roles: ['admin', 'user'] // 允许的角色
}
},
{
path: '/admin',
component: AdminLayout,
meta: {
requiresAuth: true,
roles: ['admin'] // 仅管理员
},
children: [
{
path: 'users',
component: UserManagement,
meta: { permission: 'user:view' }
},
{
path: 'settings',
component: Settings,
meta: { permission: 'system:settings' }
}
]
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 路由守卫
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
// 需要登录
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
return next({ name: 'Login', query: { redirect: to.fullPath } });
}
// 角色验证
if (to.meta.roles) {
const hasRole = to.meta.roles.some(role => userStore.roles.includes(role));
if (!hasRole) {
return next({ name: 'Forbidden' });
}
}
// 权限验证
if (to.meta.permission) {
if (!userStore.hasPermission(to.meta.permission)) {
return next({ name: 'Forbidden' });
}
}
next();
});
2. 动态路由:
// stores/permission.js
import { defineStore } from 'pinia';
export const usePermissionStore = defineStore('permission', {
state: () => ({
routes: [],
addRoutes: []
}),
actions: {
async generateRoutes(roles) {
// 根据角色过滤路由
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
this.addRoutes = accessedRoutes;
this.routes = constantRoutes.concat(accessedRoutes);
return accessedRoutes;
}
}
});
// 过滤路由
function filterAsyncRoutes(routes, roles) {
const res = [];
routes.forEach(route => {
const tmp = { ...route };
if (hasPermission(roles, tmp)) {
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles);
}
res.push(tmp);
}
});
return res;
}
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {
return roles.some(role => route.meta.roles.includes(role));
}
return true;
}
3. 按钮权限指令:
// directives/permission.js
export const permission = {
mounted(el, binding) {
const { value } = binding;
const userStore = useUserStore();
if (value && value instanceof Array && value.length > 0) {
const permissions = userStore.permissions;
const hasPermission = value.some(permission => {
return permissions.includes(permission);
});
if (!hasPermission) {
el.style.display = 'none';
// 或者 el.parentNode && el.parentNode.removeChild(el);
}
}
}
};
// 使用
<button v-permission="['user:edit']">编辑</button>
<button v-permission="['user:delete']">删除</button>
4. 权限检查组合函数:
// composables/usePermission.js
import { computed } from 'vue';
import { useUserStore } from '@/stores/user';
export function usePermission() {
const userStore = useUserStore();
const hasPermission = (permission) => {
return userStore.permissions.includes(permission);
};
const hasAnyPermission = (permissions) => {
return permissions.some(p => userStore.permissions.includes(p));
};
const hasAllPermissions = (permissions) => {
return permissions.every(p => userStore.permissions.includes(p));
};
const hasRole = (role) => {
return userStore.roles.includes(role);
};
return {
hasPermission,
hasAnyPermission,
hasAllPermissions,
hasRole
};
}
// 使用
<script setup>
const { hasPermission } = usePermission();
</script>
<template>
<button v-if="hasPermission('user:edit')">编辑</button>
</template>
▶ 如何实现一个实时数据更新的聊天应用?
技术方案:
- WebSocket 实时通信
- Pinia 状态管理
- 虚拟滚动处理大量消息
- 消息分页加载
实现代码:
// composables/useWebSocket.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useWebSocket(url) {
const ws = ref(null);
const connected = ref(false);
const messages = ref([]);
const connect = () => {
ws.value = new WebSocket(url);
ws.value.onopen = () => {
connected.value = true;
console.log('WebSocket connected');
};
ws.value.onmessage = (event) => {
const data = JSON.parse(event.data);
messages.value.push(data);
};
ws.value.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.value.onclose = () => {
connected.value = false;
console.log('WebSocket disconnected');
// 重连逻辑
setTimeout(connect, 3000);
};
};
const send = (data) => {
if (ws.value && connected.value) {
ws.value.send(JSON.stringify(data));
}
};
const disconnect = () => {
if (ws.value) {
ws.value.close();
}
};
onMounted(() => {
connect();
});
onUnmounted(() => {
disconnect();
});
return {
connected,
messages,
send,
disconnect
};
}
<!-- ChatRoom.vue -->
<template>
<div class="chat-room">
<div class="chat-header">
<h2>{{ roomName }}</h2>
<span :class="{ online: connected }">
{{ connected ? '在线' : '离线' }}
</span>
</div>
<!-- 消息列表 -->
<div class="message-list" ref="messageList">
<virtual-scroller
:items="messages"
:item-height="80"
class="scroller"
>
<template #default="{ item }">
<div :class="['message', item.isMine ? 'mine' : 'other']">
<div class="avatar">
<img :src="item.user.avatar" />
</div>
<div class="content">
<div class="user-info">
<span class="username">{{ item.user.name }}</span>
<span class="time">{{ formatTime(item.timestamp) }}</span>
</div>
<div class="text">{{ item.content }}</div>
</div>
</div>
</template>
</virtual-scroller>
</div>
<!-- 输入框 -->
<div class="input-area">
<el-input
v-model="inputMessage"
type="textarea"
:rows="3"
placeholder="输入消息..."
@keydown.enter.prevent="sendMessage"
/>
<el-button type="primary" @click="sendMessage">发送</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, nextTick, watch } from 'vue';
import { useWebSocket } from '@/composables/useWebSocket';
import { useChatStore } from '@/stores/chat';
const props = defineProps({
roomId: String
});
const chatStore = useChatStore();
const { connected, messages, send } = useWebSocket(`ws://localhost:3000/chat/${props.roomId}`);
const inputMessage = ref('');
const messageList = ref(null);
const roomName = computed(() => chatStore.getRoomName(props.roomId));
const sendMessage = () => {
if (!inputMessage.value.trim()) return;
const message = {
type: 'message',
roomId: props.roomId,
content: inputMessage.value,
timestamp: Date.now()
};
send(message);
inputMessage.value = '';
};
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString();
};
// 自动滚动到底部
watch(messages, async () => {
await nextTick();
if (messageList.value) {
messageList.value.scrollTop = messageList.value.scrollHeight;
}
});
</script>
<style scoped>
.chat-room {
display: flex;
flex-direction: column;
height: 100vh;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
display: flex;
margin-bottom: 20px;
}
.message.mine {
flex-direction: row-reverse;
}
.input-area {
display: flex;
gap: 10px;
padding: 20px;
border-top: 1px solid #eee;
}
</style>
Store 管理:
// stores/chat.js
export const useChatStore = defineStore('chat', {
state: () => ({
rooms: {},
currentRoom: null,
onlineUsers: []
}),
actions: {
addMessage(roomId, message) {
if (!this.rooms[roomId]) {
this.rooms[roomId] = { messages: [] };
}
this.rooms[roomId].messages.push(message);
},
loadHistory(roomId, page = 1) {
return api.getChatHistory(roomId, page);
}
}
});
其他重要问题
▶ nextTick 的作用和原理?
作用: 在下次 DOM 更新循环结束之后执行延迟回调,用于获取更新后的 DOM。
原理:
- Vue 的 DOM 更新是异步的
- nextTick 将回调推迟到下一个 DOM 更新周期
- 内部使用微任务(Promise.then、MutationObserver)或宏任务(setImmediate、setTimeout)
使用场景:
// 修改数据后立即操作 DOM
this.message = 'Updated';
this.$nextTick(() => {
// DOM 已更新
console.log(this.$el.textContent); // 'Updated'
});
// Composition API
import { nextTick } from 'vue';
message.value = 'Updated';
await nextTick();
console.log(document.getElementById('message').textContent);
▶ Vue 的 diff 算法原理?
核心思想:
- 同层比较,不跨层级
- 通过 key 识别节点
- 最小化 DOM 操作
比较流程:
- 比较新旧节点是否相同(tag、key、isComment 等)
- 相同则复用,不同则创建新节点
- 比较子节点(双端比较算法)
优化策略:
- 使用唯一 key 帮助识别节点
- 静态标记(PatchFlag)
- 静态提升(hoistStatic)
- 缓存事件处理函数
// 双端比较
function updateChildren(oldCh, newCh) {
let oldStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let newStartIdx = 0;
let newEndIdx = newCh.length - 1;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 四种快速比较
if (sameVnode(oldStart, newStart)) {
// 头头比较
} else if (sameVnode(oldEnd, newEnd)) {
// 尾尾比较
} else if (sameVnode(oldStart, newEnd)) {
// 头尾比较
} else if (sameVnode(oldEnd, newStart)) {
// 尾头比较
} else {
// 乱序比较(使用 key)
}
}
}
▶ Vue 3 的 Teleport 组件有什么用?
作用: 将组件的 HTML 渲染到 DOM 中的任意位置,而不是当前组件的 DOM 树中。
使用场景:
- 模态对话框
- 通知/提示
- 下拉菜单
- 全屏组件
<template>
<button @click="open = true">打开对话框</button>
<teleport to="body">
<div v-if="open" class="modal">
<h2>模态对话框</h2>
<button @click="open = false">关闭</button>
</div>
</teleport>
</template>
<script setup>
import { ref } from 'vue';
const open = ref(false);
</script>
多个 Teleport 到同一目标:
<teleport to="#modals">
<div>Modal 1</div>
</teleport>
<teleport to="#modals">
<div>Modal 2</div>
</teleport>
<!-- 渲染结果 -->
<div id="modals">
<div>Modal 1</div>
<div>Modal 2</div>
</div>
▶ 如何在 Vue 中使用 TypeScript?有什么最佳实践?
1. 项目配置:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
}
}
2. 组件类型定义:
<script setup lang="ts">
import { ref, computed, PropType } from 'vue';
// Props 类型
interface Props {
title: string;
count?: number;
items: string[];
}
const props = withDefaults(defineProps<Props>(), {
count: 0
});
// Emit 类型
const emit = defineEmits<{
(e: 'update', value: string): void;
(e: 'delete', id: number): void;
}>();
// Ref 类型
const message = ref<string>('');
const numbers = ref<number[]>([]);
// Computed 类型
const doubleCount = computed<number>(() => props.count * 2);
// 自定义类型
interface User {
id: number;
name: string;
email: string;
}
const user = ref<User>({
id: 1,
name: 'John',
email: 'john@example.com'
});
</script>
3. 组合函数类型:
// composables/useCounter.ts
import { ref, Ref } from 'vue';
export function useCounter(initialValue = 0): {
count: Ref<number>;
increment: () => void;
decrement: () => void;
} {
const count = ref(initialValue);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
return {
count,
increment,
decrement
};
}
4. Pinia Store 类型:
// stores/user.ts
import { defineStore } from 'pinia';
interface User {
id: number;
name: string;
email: string;
}
interface UserState {
user: User | null;
token: string | null;
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
user: null,
token: null
}),
getters: {
isAuthenticated(): boolean {
return this.token !== null;
}
},
actions: {
async login(email: string, password: string): Promise<void> {
// ...
}
}
});
▶ 如何测试 Vue 组件?
使用 Vitest + Vue Test Utils:
// MyComponent.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('renders properly', () => {
const wrapper = mount(MyComponent, {
props: {
title: 'Hello Vitest'
}
});
expect(wrapper.text()).toContain('Hello Vitest');
});
it('increments count when button is clicked', async () => {
const wrapper = mount(MyComponent);
const button = wrapper.find('button');
await button.trigger('click');
expect(wrapper.vm.count).toBe(1);
expect(wrapper.text()).toContain('Count: 1');
});
it('emits custom event', async () => {
const wrapper = mount(MyComponent);
await wrapper.vm.someMethod();
expect(wrapper.emitted()).toHaveProperty('custom-event');
expect(wrapper.emitted('custom-event')?.[0]).toEqual(['data']);
});
});
测试组合函数:
// useCounter.spec.ts
import { describe, it, expect } from 'vitest';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('increments count', () => {
const { count, increment } = useCounter(0);
expect(count.value).toBe(0);
increment();
expect(count.value).toBe(1);
});
});
测试 Pinia Store:
// userStore.spec.ts
import { setActivePinia, createPinia } from 'pinia';
import { useUserStore } from './user';
describe('User Store', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it('logs in user', async () => {
const store = useUserStore();
await store.login('test@example.com', 'password');
expect(store.isAuthenticated).toBe(true);
expect(store.user).toBeDefined();
});
});
▶ Vue的响应式原理是什么?Vue 2和Vue 3有什么区别?
Vue的响应式系统是Vue的核心特性,它能够自动追踪依赖并在数据变化时更新视图。
Vue 2的响应式原理(Object.defineProperty):
// Vue 2使用Object.defineProperty实现响应式
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// 依赖收集
console.log(`访问了 ${key}`);
return val;
},
set(newVal) {
// 触发更新
console.log(`${key} 从 ${val} 变为 ${newVal}`);
val = newVal;
// 通知所有依赖更新
}
});
}
// 示例
const data = {};
defineReactive(data, 'count', 0);
data.count; // 访问了 count
data.count = 1; // count 从 0 变为 1
Vue 2的局限性:
- 无法检测对象属性的添加或删除:
const vm = new Vue({
data: {
user: {
name: 'Alice'
}
}
});
// 这个不是响应式的❌
vm.user.age = 25;
// 需要使用Vue.set✅
Vue.set(vm.user, 'age', 25);
// 或
vm.$set(vm.user, 'age', 25);
- 数组问题:
// 通过索引设置项不是响应式的❌
vm.items[0] = 'new item';
// 需要使用Vue.set或数组方法✅
Vue.set(vm.items, 0, 'new item');
vm.items.splice(0, 1, 'new item');
Vue 3的响应式原理(Proxy):
// Vue 3使用Proxy实现响应式
function reactive(target) {
return new Proxy(target, {
get(target, key) {
// 依赖收集
console.log(`访问了 ${key}`);
const result = Reflect.get(target, key);
// 如果是对象,递归代理
if (typeof result === 'object' && result !== null) {
return reactive(result);
}
return result;
},
set(target, key, value) {
// 触发更新
console.log(`${key} 设置为 ${value}`);
const result = Reflect.set(target, key, value);
// 通知所有依赖更新
return result;
},
deleteProperty(target, key) {
// 删除属性也能检测到
console.log(`删除了 ${key}`);
return Reflect.deleteProperty(target, key);
}
});
}
// 示例
const data = reactive({ count: 0 });
data.count; // 访问了 count
data.count = 1; // count 设置为 1
data.newProp = 'test'; // 可以检测到新属性
delete data.count; // 可以检测到删除
Vue 3的优势:
- 可以检测属性的添加和删除:
const state = reactive({
user: { name: 'Alice' }
});
// 直接添加属性,完全响应式✅
state.user.age = 25;
// 删除属性也是响应式的✅
delete state.user.name;
- 可以监听数组索引和length的变化:
const state = reactive({
items: ['a', 'b', 'c']
});
// 通过索引设置,完全响应式✅
state.items[0] = 'x';
// 修改length,完全响应式✅
state.items.length = 1;
- 更好的性能:
- Vue 2需要递归遍历所有属性添加getter/setter
- Vue 3使用Proxy代理整个对象,惰性递归
响应式API对比:
Vue 2:
export default {
data() {
return {
count: 0,
user: { name: 'Alice' }
};
},
methods: {
increment() {
this.count++;
},
addAge() {
// 需要$set
this.$set(this.user, 'age', 25);
}
}
};
Vue 3 Composition API:
import { reactive, ref } from 'vue';
export default {
setup() {
// reactive用于对象
const state = reactive({
count: 0,
user: { name: 'Alice' }
});
// ref用于基本类型
const count = ref(0);
function increment() {
count.value++;
}
function addAge() {
// 直接添加,无需特殊处理✅
state.user.age = 25;
}
return {
state,
count,
increment,
addAge
};
}
};
ref vs reactive:
// ref:适合基本类型和单个值
const count = ref(0);
count.value++; // 需要.value访问
// reactive:适合对象和复杂数据结构
const state = reactive({
count: 0,
user: { name: 'Alice' }
});
state.count++; // 直接访问,无需.value
// 注意:reactive的解构会失去响应式❌
const { count } = state;
count++; // 不会触发更新
// 解决方案1:使用toRefs✅
const { count } = toRefs(state);
count.value++; // 响应式
// 解决方案2:使用computed✅
const count = computed(() => state.count);
性能对比:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 初始化性能 | 慢(递归遍历) | 快(惰性代理) |
| 内存占用 | 高(每个属性都有getter/setter) | 低(只代理对象本身) |
| 添加属性 | 不响应(需要$set) | 响应 |
| 删除属性 | 不响应(需要$delete) | 响应 |
| 数组索引 | 不响应 | 响应 |
| 深层对象 | 初始化时全部转换 | 访问时才转换(惰性) |
实战建议:
- Vue 2项目:使用
Vue.set和Vue.delete处理动态属性 - Vue 3项目:优先使用Composition API和
reactive/ref - 基本类型用
ref,对象用reactive - 需要解构时使用
toRefs - 复杂逻辑封装为composables
▶ Vue的虚拟DOM和diff算法是如何工作的?
虚拟DOM(Virtual DOM)是Vue实现高效渲染的核心机制。
什么是虚拟DOM:
虚拟DOM是用JavaScript对象来描述真实DOM的树形结构。
// 真实DOM
<div class="container">
<h1>Hello</h1>
<p>World</p>
</div>
// 虚拟DOM(简化版)
{
tag: 'div',
props: { class: 'container' },
children: [
{
tag: 'h1',
children: 'Hello'
},
{
tag: 'p',
children: 'World'
}
]
}
为什么需要虚拟DOM:
- 性能优化:批量更新,减少DOM操作
- 跨平台:可以渲染到不同平台(Web、Native、SSR)
- 方便追踪变化:diff算法找出最小变化
Vue的diff算法:
Vue使用双端比较算法(双指针),比React的diff更高效。
核心原理:
// 简化的diff算法
function patch(oldVNode, newVNode) {
// 1. 新旧节点类型不同,直接替换
if (oldVNode.tag !== newVNode.tag) {
return replaceNode(oldVNode, newVNode);
}
// 2. 文本节点
if (typeof newVNode.children === 'string') {
if (oldVNode.children !== newVNode.children) {
updateText(oldVNode, newVNode.children);
}
return;
}
// 3. 子节点diff
updateChildren(oldVNode.children, newVNode.children);
}
双端比较算法:
// 四个指针:旧前、旧后、新前、新后
function updateChildren(oldChildren, newChildren) {
let oldStartIdx = 0;
let oldEndIdx = oldChildren.length - 1;
let newStartIdx = 0;
let newEndIdx = newChildren.length - 1;
let oldStartVNode = oldChildren[0];
let oldEndVNode = oldChildren[oldEndIdx];
let newStartVNode = newChildren[0];
let newEndVNode = newChildren[newEndIdx];
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (sameVNode(oldStartVNode, newStartVNode)) {
// 情况1:新旧头节点相同
patch(oldStartVNode, newStartVNode);
oldStartVNode = oldChildren[++oldStartIdx];
newStartVNode = newChildren[++newStartIdx];
} else if (sameVNode(oldEndVNode, newEndVNode)) {
// 情况2:新旧尾节点相同
patch(oldEndVNode, newEndVNode);
oldEndVNode = oldChildren[--oldEndIdx];
newEndVNode = newChildren[--newEndIdx];
} else if (sameVNode(oldStartVNode, newEndVNode)) {
// 情况3:旧头与新尾相同(节点右移)
patch(oldStartVNode, newEndVNode);
moveNode(oldStartVNode, oldEndVNode.nextSibling);
oldStartVNode = oldChildren[++oldStartIdx];
newEndVNode = newChildren[--newEndIdx];
} else if (sameVNode(oldEndVNode, newStartVNode)) {
// 情况4:旧尾与新头相同(节点左移)
patch(oldEndVNode, newStartVNode);
moveNode(oldEndVNode, oldStartVNode);
oldEndVNode = oldChildren[--oldEndIdx];
newStartVNode = newChildren[++newStartIdx];
} else {
// 情况5:都不相同,使用key查找
const idxInOld = findIdxInOld(newStartVNode, oldChildren);
if (idxInOld) {
const vnodeToMove = oldChildren[idxInOld];
patch(vnodeToMove, newStartVNode);
moveNode(vnodeToMove, oldStartVNode);
oldChildren[idxInOld] = undefined;
} else {
// 新节点,创建
createNode(newStartVNode, oldStartVNode);
}
newStartVNode = newChildren[++newStartIdx];
}
}
// 处理剩余节点
if (oldStartIdx > oldEndIdx) {
// 新节点有剩余,添加
addNodes(newChildren, newStartIdx, newEndIdx);
} else if (newStartIdx > newEndIdx) {
// 旧节点有剩余,删除
removeNodes(oldChildren, oldStartIdx, oldEndIdx);
}
}
key的重要性:
// 没有key - 错误的diff❌
<ul>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
// 在最前面插入D
<ul>
<li>D</li>
<li>A</li>
<li>B</li>
<li>C</li>
</ul>
// Vue会认为:
// A→D (更新文本)
// B→A (更新文本)
// C→B (更新文本)
// 新建C
// 效率很低!
// 有key - 正确的diff✅
<ul>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
<ul>
<li key="d">D</li>
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>
</ul>
// Vue会认为:
// 新建D,A、B、C都不变
// 只需要一次DOM插入操作!
不要使用index作为key:
// 错误示例❌
<li v-for="(item, index) in items" :key="index">
{{ item }}
</li>
// 问题:删除第一项时,所有index都变了
// items = ['A', 'B', 'C']
// 删除'A'后 items = ['B', 'C']
// B的key从1变为0,C的key从2变为1
// Vue会误以为内容发生了变化
// 正确示例✅
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
Vue 2 vs Vue 3的diff优化:
Vue 2:
- 双端比较算法
- 每次都全量diff所有子节点
Vue 3:
- 静态标记(PatchFlag)
- 静态提升(hoisting)
- 事件监听缓存
- Block Tree(减少diff范围)
// Vue 3的优化
// 编译前
<div>
<h1>静态内容</h1>
<p>{{ dynamic }}</p>
</div>
// 编译后(简化)
const _hoisted_1 = /*#__PURE__*/ _createElementVNode("h1", null, "静态内容", -1);
function render() {
return _createElementBlock("div", null, [
_hoisted_1, // 静态节点提升,不参与diff
_createElementVNode("p", null, _toDisplayString(dynamic), 1 /* TEXT */)
// PatchFlag标记:1表示只有文本内容是动态的
]);
}
性能对比:
| 操作 | 真实DOM | 虚拟DOM |
|---|---|---|
| 创建节点 | 慢 | 快(JavaScript对象) |
| 更新节点 | 慢(重排重绘) | 快(批量更新) |
| 查找节点 | 慢 | 快(内存操作) |
| 大量更新 | 很慢 | 快(diff+批量) |
最佳实践:
✅ 列表渲染必须使用唯一的key ✅ key使用稳定的唯一标识(如id),不要使用index ✅ 避免在key中使用随机值或Date.now() ✅ 静态内容提取到组件外部 ✅ 合理使用v-once和v-memo优化
▶ Vue的生命周期钩子执行顺序是怎样的?父子组件呢?
Vue组件的生命周期是Vue实例从创建到销毁的完整过程。
Vue 2生命周期:
export default {
// 1. 创建阶段
beforeCreate() {
// 实例初始化之后,数据观测和事件配置之前
// 访问不到data、computed、methods
console.log('1. beforeCreate');
},
created() {
// 实例创建完成,数据观测、属性和方法已配置
// 可以访问data、computed、methods
// 还未挂载DOM,无法访问$el
console.log('2. created');
// 常用:发起API请求
},
// 2. 挂载阶段
beforeMount() {
// 挂载开始之前,render函数首次被调用
// 模板编译完成,虚拟DOM已创建
console.log('3. beforeMount');
},
mounted() {
// 实例挂载完成,真实DOM已创建
// 可以访问this.$el和DOM元素
console.log('4. mounted');
// 常用:DOM操作、初始化第三方库、开启定时器
},
// 3. 更新阶段
beforeUpdate() {
// 数据更新时调用,发生在虚拟DOM重新渲染之前
// 可以在这里访问更新前的DOM
console.log('5. beforeUpdate');
},
updated() {
// 数据更新后,虚拟DOM重新渲染和patch之后
// DOM已经更新完成
console.log('6. updated');
// 注意:避免在这里修改数据,可能导致无限循环
},
// 4. 销毁阶段
beforeDestroy() {
// 实例销毁之前调用,实例仍然完全可用
console.log('7. beforeDestroy');
// 常用:清理定时器、取消订阅、解绑事件监听
},
destroyed() {
// 实例销毁后调用
// 所有指令解绑、事件监听器移除、子实例销毁
console.log('8. destroyed');
}
};
Vue 3生命周期(Composition API):
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue';
export default {
setup() {
// setup()在beforeCreate之前执行
console.log('setup');
onBeforeMount(() => {
console.log('onBeforeMount');
});
onMounted(() => {
console.log('onMounted');
// 常用:DOM操作、API请求
});
onBeforeUpdate(() => {
console.log('onBeforeUpdate');
});
onUpdated(() => {
console.log('onUpdated');
});
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
// 常用:清理工作
});
onUnmount(() => {
console.log('onUnmounted');
});
return {};
}
};
Options API vs Composition API对照:
| Options API | Composition API | 说明 |
|---|---|---|
| beforeCreate | setup() | Composition API中setup在beforeCreate之前 |
| created | setup() | 在setup中可以直接写逻辑 |
| beforeMount | onBeforeMount | 挂载前 |
| mounted | onMounted | 挂载后 |
| beforeUpdate | onBeforeUpdate | 更新前 |
| updated | onUpdated | 更新后 |
| beforeDestroy | onBeforeUnmount | 卸载前(Vue 3改名) |
| destroyed | onUnmounted | 卸载后(Vue 3改名) |
父子组件生命周期执行顺序:
// Parent.vue
export default {
name: 'Parent',
beforeCreate() { console.log('1. Parent beforeCreate'); },
created() { console.log('2. Parent created'); },
beforeMount() { console.log('3. Parent beforeMount'); },
mounted() { console.log('7. Parent mounted'); },
beforeUpdate() { console.log('Parent beforeUpdate'); },
updated() { console.log('Parent updated'); },
beforeDestroy() { console.log('Parent beforeDestroy'); },
destroyed() { console.log('Parent destroyed'); }
};
// Child.vue
export default {
name: 'Child',
beforeCreate() { console.log(' 4. Child beforeCreate'); },
created() { console.log(' 5. Child created'); },
beforeMount() { console.log(' 6. Child beforeMount'); },
mounted() { console.log(' 7. Child mounted'); },
beforeUpdate() { console.log(' Child beforeUpdate'); },
updated() { console.log(' Child updated'); },
beforeDestroy() { console.log(' Child beforeDestroy'); },
destroyed() { console.log(' Child destroyed'); }
};
加载渲染过程:
1. Parent beforeCreate
2. Parent created
3. Parent beforeMount
4. Child beforeCreate
5. Child created
6. Child beforeMount
7. Child mounted
8. Parent mounted
子组件更新过程:
Parent beforeUpdate
Child beforeUpdate
Child updated
Parent updated
父组件更新过程:
Parent beforeUpdate
Parent updated
销毁过程:
Parent beforeDestroy
Child beforeDestroy
Child destroyed
Parent destroyed
记忆口诀:
- 加载渲染:父before→父create→父beforeMount→子全部→父mounted
- 子组件更新:父before→子before→子update→父update
- 父组件更新:父before→父update
- 销毁:父before→子before→子destroy→父destroy
常见应用场景:
export default {
created() {
// ✅ 发起API请求(不依赖DOM)
this.fetchData();
},
mounted() {
// ✅ DOM操作
this.$refs.input.focus();
// ✅ 初始化第三方库(需要DOM)
new Chart(this.$refs.chart, {...});
// ✅ 开启定时器
this.timer = setInterval(() => {
this.fetchData();
}, 5000);
// ✅ 添加事件监听
window.addEventListener('resize', this.handleResize);
},
beforeDestroy() {
// ✅ 清理定时器
clearInterval(this.timer);
// ✅ 移除事件监听
window.removeEventListener('resize', this.handleResize);
// ✅ 取消未完成的请求
this.cancelToken.cancel();
}
};
最佳实践:
✅ API请求优先放在created(更早)
✅ 需要DOM操作必须在mounted之后
✅ 定时器、事件监听记得在beforeDestroy清理
✅ 避免在updated中修改数据
✅ 父子组件通信要考虑生命周期顺序
▶ Vue Router的导航守卫有哪些?执行顺序是什么?
导航守卫是Vue Router提供的路由跳转过程中的钩子函数,用于控制路由的访问权限。
导航守卫类型:
1. 全局守卫:
// router/index.js
const router = createRouter({...});
// 全局前置守卫
router.beforeEach((to, from, next) => {
console.log('全局beforeEach');
// 权限验证
if (to.meta.requiresAuth && !isLoggedIn()) {
next('/login'); // 重定向到登录页
} else {
next(); // 继续导航
}
});
// 全局解析守卫
router.beforeResolve((to, from, next) => {
console.log('全局beforeResolve');
next();
});
// 全局后置钩子
router.afterEach((to, from) => {
console.log('全局afterEach');
// 修改页面标题
document.title = to.meta.title || '默认标题';
// 发送页面浏览统计
});
2. 路由独享守卫:
const routes = [
{
path: '/admin',
component: Admin,
// 路由独享守卫
beforeEnter: (to, from, next) => {
console.log('路由独享beforeEnter');
// 只对进入该路由时触发
if (hasAdminPermission()) {
next();
} else {
next('/403');
}
}
}
];
3. 组件内守卫:
export default {
// 进入组件前
beforeRouteEnter(to, from, next) {
console.log('组件beforeRouteEnter');
// 此时组件实例还未创建,无法访问this
next(vm => {
// 通过vm访问组件实例
vm.fetchData();
});
},
// 路由更新时(同一组件,参数变化)
beforeRouteUpdate(to, from, next) {
console.log('组件beforeRouteUpdate');
// 可以访问this
this.fetchData(to.params.id);
next();
},
// 离开组件时
beforeRouteLeave(to, from, next) {
console.log('组件beforeRouteLeave');
// 表单未保存提示
if (this.hasUnsavedChanges) {
const answer = window.confirm('有未保存的更改,确定离开?');
if (answer) {
next();
} else {
next(false); // 取消导航
}
} else {
next();
}
}
};
完整导航解析流程:
// 从 /home 导航到 /about
1. 导航被触发
2. 在失活的组件里调用 beforeRouteLeave 守卫
→ Home组件的beforeRouteLeave
3. 调用全局的 beforeEach 守卫
→ router.beforeEach
4. 在重用的组件里调用 beforeRouteUpdate 守卫
→ 如果是参数变化但组件复用
5. 在路由配置里调用 beforeEnter
→ About路由的beforeEnter
6. 解析异步路由组件
→ 如果About是懒加载的
7. 在被激活的组件里调用 beforeRouteEnter
→ About组件的beforeRouteEnter
8. 调用全局的 beforeResolve 守卫
→ router.beforeResolve
9. 导航被确认
10. 调用全局的 afterEach 钩子
→ router.afterEach
11. 触发 DOM 更新
12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数
→ next(vm => { ... })
实际案例:权限验证:
// router/index.js
import { createRouter } from 'vue-router';
import { useUserStore } from '@/stores/user';
const router = createRouter({
routes: [
{
path: '/',
component: Home,
meta: { requiresAuth: false }
},
{
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true,
roles: ['admin', 'user']
}
},
{
path: '/admin',
component: Admin,
meta: {
requiresAuth: true,
roles: ['admin']
}
}
]
});
// 全局前置守卫:权限验证
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
// 页面加载进度条
NProgress.start();
// 1. 检查是否需要登录
if (to.meta.requiresAuth) {
if (!userStore.isLoggedIn) {
// 未登录,跳转登录页
next({
path: '/login',
query: { redirect: to.fullPath } // 登录后跳回
});
return;
}
// 2. 检查角色权限
if (to.meta.roles && !to.meta.roles.includes(userStore.role)) {
// 无权限,跳转403页面
next('/403');
return;
}
}
next(); // 允许导航
});
// 全局后置钩子:完成进度条
router.afterEach(() => {
NProgress.done();
});
实际案例:表单离开提示:
// components/EditForm.vue
export default {
data() {
return {
formData: {},
originalData: {},
isDirty: false
};
},
watch: {
formData: {
deep: true,
handler() {
// 检测表单是否被修改
this.isDirty = JSON.stringify(this.formData) !== JSON.stringify(this.originalData);
}
}
},
beforeRouteLeave(to, from, next) {
if (this.isDirty) {
const answer = window.confirm('表单有未保存的更改,确定离开吗?');
if (answer) {
next();
} else {
next(false); // 取消导航
}
} else {
next();
}
},
methods: {
async save() {
await this.saveData();
this.isDirty = false;
this.originalData = JSON.parse(JSON.stringify(this.formData));
}
}
};
Composition API中使用导航守卫:
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';
export default {
setup() {
const isDirty = ref(false);
// 组件内守卫
onBeforeRouteLeave((to, from) => {
if (isDirty.value) {
const answer = window.confirm('有未保存的更改,确定离开?');
if (!answer) return false; // 取消导航
}
});
onBeforeRouteUpdate((to, from) => {
// 路由参数变化时
fetchData(to.params.id);
});
return { isDirty };
}
};
常见应用场景:
✅ beforeEach:登录验证、权限检查、页面访问统计 ✅ beforeResolve:获取数据(所有守卫都解析完) ✅ afterEach:修改页面标题、发送统计、关闭加载动画 ✅ beforeEnter:特定路由的权限验证 ✅ beforeRouteLeave:表单离开提示、清理定时器 ✅ beforeRouteUpdate:同组件路由参数变化时更新数据
最佳实践:
✅ 守卫中必须调用next(),否则导航会卡住
✅ 异步操作使用async/await
✅ 避免在afterEach中做耗时操作
✅ 合理使用守卫级别(全局 vs 路由 vs 组件)
✅ 权限验证放在全局守卫,具体逻辑放在路由独享守卫
▶ Vue组件通信有哪些方式?各自的应用场景是什么?
Vue组件间通信是Vue开发中的核心问题,根据组件关系有不同的解决方案。
1. Props / Emit(父子组件):
// 父组件 Parent.vue
<template>
<Child
:message="parentMsg"
:count="count"
@update="handleUpdate"
@increment="count++"
/>
</template>
<script>
export default {
data() {
return {
parentMsg: 'Hello from parent',
count: 0
};
},
methods: {
handleUpdate(newValue) {
this.parentMsg = newValue;
}
}
};
</script>
// 子组件 Child.vue
<template>
<div>
<p>{{ message }}</p>
<p>Count: {{ count }}</p>
<button @click="updateParent">Update Parent</button>
<button @click="$emit('increment')">Increment</button>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
},
methods: {
updateParent() {
this.$emit('update', 'New message from child');
}
}
};
</script>
应用场景:
- ✅ 父→子传递数据
- ✅ 子→父传递事件
- ✅ 简单的双向绑定(v-model)
2. v-model(双向绑定):
// Vue 2
// 父组件
<CustomInput v-model="searchText" />
// 等价于
<CustomInput
:value="searchText"
@input="searchText = $event"
/>
// 子组件
export default {
props: ['value'],
methods: {
updateValue(e) {
this.$emit('input', e.target.value);
}
}
};
// Vue 3
// 父组件
<CustomInput v-model="searchText" />
// 等价于
<CustomInput
:modelValue="searchText"
@update:modelValue="searchText = $event"
/>
// 子组件
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
setup(props, { emit }) {
const updateValue = (e) => {
emit('update:modelValue', e.target.value);
};
return { updateValue };
}
};
// Vue 3支持多个v-model
<UserForm
v-model:name="userName"
v-model:email="userEmail"
/>
应用场景:
- ✅ 表单输入组件
- ✅ 需要双向绑定的自定义组件
3. Ref(父访问子):
// 父组件
<template>
<Child ref="childRef" />
<button @click="callChildMethod">Call Child Method</button>
</template>
<script>
export default {
methods: {
callChildMethod() {
// 直接调用子组件的方法
this.$refs.childRef.someMethod();
// 访问子组件的数据
console.log(this.$refs.childRef.someData);
}
},
mounted() {
// 可以在mounted中访问ref
this.$refs.childRef.init();
}
};
</script>
// 子组件
export default {
data() {
return {
someData: 'Child data'
};
},
methods: {
someMethod() {
console.log('Child method called');
},
init() {
console.log('Child initialized');
}
}
};
Composition API:
// 父组件
import { ref, onMounted } from 'vue';
const childRef = ref(null);
onMounted(() => {
childRef.value.someMethod();
});
// 子组件需要使用defineExpose暴露
import { defineExpose } from 'vue';
const someMethod = () => {
console.log('Called');
};
defineExpose({
someMethod
});
应用场景:
- ✅ 父组件调用子组件方法
- ✅ 父组件访问子组件数据
- ⚠️ 避免过度使用,破坏组件封装性
4. Provide / Inject(祖先→后代):
// 祖先组件
export default {
provide() {
return {
theme: 'dark',
user: this.currentUser,
// 响应式数据(Vue 2需要包装成对象)
userData: () => this.currentUser
};
},
data() {
return {
currentUser: { name: 'Alice' }
};
}
};
// Vue 3 Composition API
import { provide, ref, readonly } from 'vue';
export default {
setup() {
const theme = ref('dark');
const user = ref({ name: 'Alice' });
// 提供响应式数据
provide('theme', readonly(theme));
provide('user', user);
// 提供方法
provide('updateTheme', (newTheme) => {
theme.value = newTheme;
});
return { theme, user };
}
};
// 后代组件(任意层级)
export default {
inject: ['theme', 'user', 'updateTheme'],
mounted() {
console.log(this.theme); // 'dark'
this.updateTheme('light');
}
};
// Composition API
import { inject } from 'vue';
export default {
setup() {
const theme = inject('theme');
const user = inject('user');
const updateTheme = inject('updateTheme');
return { theme, user, updateTheme };
}
};
应用场景:
- ✅ 跨多层级组件通信
- ✅ 主题、国际化等全局配置
- ✅ 插件系统
5. EventBus(兄弟组件/跨级):
// Vue 2
// eventBus.js
import Vue from 'vue';
export const EventBus = new Vue();
// 组件A:发送事件
import { EventBus } from './eventBus';
EventBus.$emit('customEvent', { data: 'some data' });
// 组件B:接收事件
import { EventBus } from './eventBus';
export default {
mounted() {
EventBus.$on('customEvent', (payload) => {
console.log(payload);
});
},
beforeDestroy() {
// 记得移除监听
EventBus.$off('customEvent');
}
};
// Vue 3(推荐使用mitt库)
// npm install mitt
import mitt from 'mitt';
// eventBus.js
export const emitter = mitt();
// 组件A
import { emitter } from './eventBus';
emitter.emit('custom-event', { data: 'some data' });
// 组件B
import { onMounted, onUnmounted } from 'vue';
import { emitter } from './eventBus';
const handler = (payload) => {
console.log(payload);
};
onMounted(() => {
emitter.on('custom-event', handler);
});
onUnmounted(() => {
emitter.off('custom-event', handler);
});
应用场景:
- ✅ 兄弟组件通信
- ✅ 跨层级组件通信(简单场景)
- ⚠️ 复杂场景建议使用Vuex/Pinia
6. Vuex/Pinia(全局状态管理):
// Pinia store
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
name: 'Alice',
age: 25,
isLoggedIn: false
}),
getters: {
fullInfo: (state) => `${state.name}, ${state.age}`,
isAdult: (state) => state.age >= 18
},
actions: {
login(username) {
this.name = username;
this.isLoggedIn = true;
},
logout() {
this.name = '';
this.isLoggedIn = false;
}
}
});
// 组件中使用
import { useUserStore } from '@/stores/user';
export default {
setup() {
const userStore = useUserStore();
// 访问state
console.log(userStore.name);
// 访问getters
console.log(userStore.fullInfo);
// 调用actions
userStore.login('Bob');
return { userStore };
}
};
应用场景:
- ✅ 全局共享状态
- ✅ 复杂的状态逻辑
- ✅ 需要持久化的数据
- ✅ 多个组件共享同一数据
通信方式对比:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Props/Emit | 父子组件 | 简单直观 | 只能父子通信 |
| v-model | 双向绑定 | 语法简洁 | 仅适合表单类组件 |
| Ref | 父访问子 | 直接调用子组件 | 破坏封装性 |
| Provide/Inject | 祖先后代 | 跨层级传递 | 不易追踪数据流 |
| EventBus | 任意组件 | 灵活 | 难以维护 |
| Vuex/Pinia | 全局状态 | 集中管理 | 小项目过重 |
选择建议:
✅ 父子组件:优先使用 Props/Emit ✅ 表单组件:使用 v-model ✅ 跨层级:少量数据用 Provide/Inject,复杂状态用 Pinia ✅ 兄弟组件:简单场景用 EventBus,复杂场景用 Pinia ✅ 全局状态:使用 Pinia(Vue 3)或 Vuex(Vue 2) ✅ 避免使用 Ref:除非确实需要直接操作子组件
Excel常用函数
▶ 如何使用VLOOKUP和XLOOKUP进行数据查找?
VLOOKUP和XLOOKUP是Excel中最常用的查找函数,用于从表格中匹配数据。
VLOOKUP基础:
=VLOOKUP(查找值, 查找范围, 返回列号, [精确/近似匹配])
示例:
=VLOOKUP(A2, $D$2:$F$10, 3, FALSE)
参数说明:
- 查找值:要查找的值(如员工编号、产品ID)
- 查找范围:包含查找值和返回值的区域
- 返回列号:从查找范围左侧开始的列序号
- 精确/近似匹配:
FALSE或0:精确匹配(推荐)TRUE或1:近似匹配(需排序)
实战场景:
场景1:根据员工编号查找姓名
表格结构:
A列: 订单编号 B列: 员工编号 C列: 姓名(需查找)
查找表:
D列: 员工编号 E列: 姓名 F列: 部门
公式:
=VLOOKUP(B2, $D$2:$F$100, 2, FALSE)
场景2:防止出错(IFERROR包裹)
问题:查找不到时显示#N/A错误
解决:
=IFERROR(VLOOKUP(B2, $D$2:$F$100, 2, FALSE), "未找到")
或使用IF+ISNA:
=IF(ISNA(VLOOKUP(B2, $D$2:$F$100, 2, FALSE)), "未找到", VLOOKUP(B2, $D$2:$F$100, 2, FALSE))
场景3:多条件查找(辅助列)
需求:同时匹配部门和员工编号
步骤1:创建辅助列
# 在查找表新增列G(辅助列)
=D2&"-"&E2 # 合并部门和编号
步骤2:使用VLOOKUP查找
=VLOOKUP(A2&"-"&B2, $G$2:$H$100, 2, FALSE)
XLOOKUP优势(Excel 365/2021):
=XLOOKUP(查找值, 查找数组, 返回数组, [找不到的返回值], [匹配模式], [搜索模式])
示例:
=XLOOKUP(A2, $D$2:$D$100, $E$2:$E$100, "未找到", 0)
XLOOKUP vs VLOOKUP对比:
| 特性 | VLOOKUP | XLOOKUP |
|---|---|---|
| 查找方向 | 只能向右查找 | 任意方向 |
| 列号变化 | 需手动调整列号 | 自动适应 |
| 默认错误 | 显示#N/A | 可自定义 |
| 多条件 | 需辅助列 | 直接支持 |
| 性能 | 较慢 | 更快 |
XLOOKUP实战:
# 1. 反向查找(向左查找)
=XLOOKUP(A2, $E$2:$E$100, $D$2:$D$100)
# 2. 查找最后一个匹配项
=XLOOKUP(A2, $D$2:$D$100, $E$2:$E$100, , 0, -1)
# 3. 返回多列
=XLOOKUP(A2, $D$2:$D$100, $E$2:$G$100)
# 4. 近似匹配(成绩等级)
=XLOOKUP(A2, {0,60,70,80,90}, {"不及格","及格","中等","良好","优秀"}, , 1)
常见错误处理:
# 1. 查找范围未锁定
❌ =VLOOKUP(A2, D2:F100, 2, FALSE) # 向下拖动公式时范围会变
✅ =VLOOKUP(A2, $D$2:$F$100, 2, FALSE) # 使用绝对引用
# 2. 数据类型不匹配
# 问题:文本"001"无法匹配数字1
# 解决:统一类型
=VLOOKUP(TEXT(A2,"000"), $D$2:$F$100, 2, FALSE)
或
=VLOOKUP(VALUE(A2), $D$2:$F$100, 2, FALSE)
# 3. 空格导致无法匹配
=VLOOKUP(TRIM(A2), $D$2:$F$100, 2, FALSE)
性能优化技巧:
- 使用精确匹配:
FALSE比TRUE更快 - 限制查找范围:只包含必要列
- 避免跨表查找:整理到同一工作表
- 使用INDEX+MATCH替代(大数据量):
=INDEX($E$2:$E$100, MATCH(A2, $D$2:$D$100, 0))
▶ 如何使用IF和IFS进行条件判断?
IF函数是Excel逻辑判断的核心,用于根据条件返回不同结果。
IF函数基础:
=IF(条件, 真值, 假值)
示例:
=IF(A2>=60, "及格", "不及格")
嵌套IF(多条件):
# 成绩等级划分
=IF(A2>=90, "优秀", IF(A2>=80, "良好", IF(A2>=70, "中等", IF(A2>=60, "及格", "不及格"))))
# 更清晰的写法(按Alt+Enter换行)
=IF(A2>=90, "优秀",
IF(A2>=80, "良好",
IF(A2>=70, "中等",
IF(A2>=60, "及格", "不及格"))))
IFS函数(Excel 2019+,更简洁):
=IFS(
条件1, 结果1,
条件2, 结果2,
条件3, 结果3,
TRUE, 默认结果
)
示例:
=IFS(
A2>=90, "优秀",
A2>=80, "良好",
A2>=70, "中等",
A2>=60, "及格",
TRUE, "不及格"
)
实战场景:
场景1:薪资等级判断
# 基础判断
=IF(B2>10000, "高薪", "普通")
# 带阈值
=IF(B2>=15000, "A级", IF(B2>=10000, "B级", IF(B2>=7000, "C级", "D级")))
# IFS版本(推荐)
=IFS(
B2>=15000, "A级",
B2>=10000, "B级",
B2>=7000, "C级",
TRUE, "D级"
)
场景2:多条件AND组合
需求:年龄>=25且工资>=8000才是"目标人群"
# 方法1:嵌套IF
=IF(AND(B2>=25, C2>=8000), "目标人群", "非目标")
# 方法2:乘法(更简洁)
=IF((B2>=25)*(C2>=8000), "目标人群", "非目标")
# 方法3:IFS(多目标分级)
=IFS(
AND(B2>=25, C2>=10000), "优质",
AND(B2>=25, C2>=8000), "目标",
TRUE, "非目标"
)
场景3:多条件OR组合
需求:VIP或消费>5000元可享受折扣
# 方法1:OR函数
=IF(OR(B2="VIP", C2>5000), "9折", "原价")
# 方法2:加法
=IF((B2="VIP")+(C2>5000), "9折", "原价")
# 方法3:复杂OR+AND
=IF(OR(B2="VIP", AND(C2>5000, D2>3)), "8折", "9折")
场景4:包含文本判断
# 检查是否包含关键词
=IF(ISNUMBER(SEARCH("北京", A2)), "一线", "其他")
# 多关键词检查
=IF(OR(ISNUMBER(SEARCH("北京", A2)), ISNUMBER(SEARCH("上海", A2)), ISNUMBER(SEARCH("广州", A2))), "一线", "其他")
# 排除某些值
=IF(OR(A2="", A2="N/A", A2="无"), "缺失", A2)
常用组合函数:
# 1. IF + ISBLANK(空值判断)
=IF(ISBLANK(A2), "未填写", A2)
# 2. IF + ISERROR(错误处理)
=IF(ISERROR(A2/B2), 0, A2/B2)
# 3. IF + COUNTIF(唯一性检查)
=IF(COUNTIF($A$2:A2, A2)>1, "重复", "唯一")
# 4. IF + LEN(长度验证)
=IF(LEN(A2)=11, "手机号正确", "手机号错误")
# 5. IF + WEEKDAY(工作日判断)
=IF(OR(WEEKDAY(A2)=1, WEEKDAY(A2)=7), "周末", "工作日")
# 6. IF + MONTH(月份判断)
=IF(MONTH(A2)<=6, "上半年", "下半年")
高级技巧:
1. 区间判断(嵌套AND)
# 判断年龄是否在18-35岁之间
=IF(AND(A2>=18, A2<=35), "符合", "不符合")
# 多区间判断
=IFS(
A2<18, "未成年",
AND(A2>=18, A2<=35), "青年",
AND(A2>35, A2<=60), "中年",
A2>60, "老年"
)
2. 动态阈值
# 根据部门设置不同及格线
=IF(B2="销售", IF(C2>=8000, "达标", "未达标"), IF(C2>=5000, "达标", "未达标"))
# 使用VLOOKUP动态查找阈值
=IF(C2>=VLOOKUP(B2, $E$2:$F$10, 2, FALSE), "达标", "未达标")
3. 优先级判断
# 多个奖励规则,取最高等级
=IFS(
A2>=90, "特等奖",
A2>=80, "一等奖",
A2>=70, "二等奖",
A2>=60, "三等奖",
TRUE, "无奖励"
)
4. 组合计算
# 根据条件计算不同公式
=IF(A2="固定", B2*1000, IF(A2="提成", B2*0.1*C2, B2*500))
# 带折扣的价格计算
=IF(B2="VIP", A2*0.8, IF(B2="会员", A2*0.9, A2))
性能优化:
- 避免过深嵌套:超过3层考虑用IFS或SWITCH
- 简化逻辑:能用数学运算代替IF就不用IF
# 慢 =IF(A2>0, 1, 0) # 快 =(A2>0)*1 - 提前计算:复杂条件先用辅助列计算,再判断
常见错误:
❌ =IF(A2>60, "及格") # 缺少假值参数
✅ =IF(A2>60, "及格", "不及格")
❌ =IF(A2="VIP" OR B2>5000, "优惠", "原价") # 语法错误
✅ =IF(OR(A2="VIP", B2>5000), "优惠", "原价")
❌ =IF(AND(A2>60, <80), "中等", "其他") # 不完整表达式
✅ =IF(AND(A2>60, A2<80), "中等", "其他")
▶ 如何使用SUMIF、SUMIFS进行条件求和?
条件求和是数据分析中的常用操作,用于按条件汇总数据。
SUMIF:单条件求和:
=SUMIF(条件区域, 条件, [求和区域])
示例:
=SUMIF(B:B, "北京", C:C) # B列是"北京"的行,对应C列求和
参数说明:
- 条件区域:判断条件的单元格区域
- 条件:筛选条件(可以是数字、文本、表达式)
- 求和区域:实际求和的单元格区域(可省略,则对条件区域求和)
常用条件格式:
# 1. 等于
=SUMIF(A:A, "北京", B:B)
=SUMIF(A:A, "="&D2, B:B) # 引用单元格
# 2. 大于/小于
=SUMIF(B:B, ">1000", C:C)
=SUMIF(B:B, "<="&D2, C:C) # 引用单元格阈值
# 3. 不等于
=SUMIF(A:A, "<>北京", B:B)
# 4. 通配符
=SUMIF(A:A, "北京*", B:B) # 以"北京"开头
=SUMIF(A:A, "*公司", B:B) # 以"公司"结尾
=SUMIF(A:A, "*销售*", B:B) # 包含"销售"
# 5. 日期条件
=SUMIF(A:A, ">=2024-01-01", B:B)
=SUMIF(A:A, ">="&DATE(2024,1,1), B:B)
SUMIFS:多条件求和:
=SUMIFS(求和区域, 条件区域1, 条件1, 条件区域2, 条件2, ...)
示例:
=SUMIFS(D:D, B:B, "北京", C:C, ">1000")
# 求和D列,条件:B列="北京" 且 C列>1000
实战场景:
场景1:部门销售额统计
数据结构:
A列: 日期 B列: 部门 C列: 销售员 D列: 销售额
# 统计销售部总销售额
=SUMIF(B:B, "销售部", D:D)
# 统计特定销售员业绩
=SUMIF(C:C, "张三", D:D)
# 统计大额订单(>5000)
=SUMIF(D:D, ">5000") # 省略求和区域,对D列自身求和
=SUMIF(D:D, ">5000", D:D) # 完整写法
# 引用单元格条件
=SUMIF(B:B, F2, D:D) # F2单元格填写"销售部"
=SUMIF(D:D, ">"&G2, D:D) # G2单元格填写阈值5000
场景2:多维度交叉统计
# 销售部且销售额>5000
=SUMIFS(D:D, B:B, "销售部", D:D, ">5000")
# 北京地区的VIP客户消费
=SUMIFS(E:E, B:B, "北京", C:C, "VIP")
# 销售部张三的大额订单
=SUMIFS(D:D, B:B, "销售部", C:C, "张三", D:D, ">=5000")
# 排除某些值
=SUMIFS(D:D, B:B, "<>销售部", C:C, "<>退货")
场景3:日期范围统计
# 某月销售额
=SUMIFS(D:D, A:A, ">=2024-01-01", A:A, "<=2024-01-31")
# 使用DATE函数
=SUMIFS(D:D, A:A, ">="&DATE(2024,1,1), A:A, "<="&DATE(2024,1,31))
# 本月销售额(动态)
=SUMIFS(D:D, A:A, ">="&DATE(YEAR(TODAY()),MONTH(TODAY()),1), A:A, "<="&EOMONTH(TODAY(),0))
# 近30天销售额
=SUMIFS(D:D, A:A, ">="&TODAY()-30, A:A, "<="&TODAY())
# 本年度销售额
=SUMIFS(D:D, A:A, ">="&DATE(YEAR(TODAY()),1,1), A:A, "<="&DATE(YEAR(TODAY()),12,31))
# 上个月销售额
=SUMIFS(D:D, A:A, ">="&DATE(YEAR(EOMONTH(TODAY(),-1)),MONTH(EOMONTH(TODAY(),-1)),1), A:A, "<="&EOMONTH(TODAY(),-1))
场景4:动态条件和通配符
# 包含特定关键词
=SUMIF(B:B, "*电子*", D:D) # 包含"电子"
=SUMIF(B:B, "北京*", D:D) # 以"北京"开头
=SUMIF(C:C, "*?公司", D:D) # ?代表单个字符
# 多关键词求和(OR逻辑)
=SUMIF(B:B, "*北京*", D:D) + SUMIF(B:B, "*上海*", D:D) + SUMIF(B:B, "*广州*", D:D)
# 根据单元格动态条件
=SUMIF(B:B, "*"&F2&"*", D:D) # F2输入关键词
# 复杂文本条件
=SUMIFS(D:D, B:B, "*电子*", C:C, "*有限公司")
高级应用:
1. 跨表求和
# 汇总多个工作表
=SUMIF(Sheet1!B:B, "北京", Sheet1!D:D) + SUMIF(Sheet2!B:B, "北京", Sheet2!D:D)
# 使用3D引用(相同结构的多表)
=SUM(SUMIF(Sheet1:Sheet3!B:B, "北京", Sheet1:Sheet3!D:D))
2. 数组条件(多值匹配)
# 求和多个部门(需Ctrl+Shift+Enter,或Office 365自动)
=SUM(SUMIF(B:B, {"销售部","市场部","技术部"}, D:D))
# 或使用辅助列标记
新增列E:=IF(OR(B2="销售部", B2="市场部", B2="技术部"), "目标部门", "")
求和:=SUMIF(E:E, "目标部门", D:D)
3. 去重求和
# 对唯一值求和(假设B列有重复,求D列唯一行的和)
=SUMPRODUCT((B2:B100<>"")/COUNTIF(B2:B100, B2:B100&""), D2:D100)
# 简单场景:用SUMIF去重
在辅助列:=IF(COUNTIF($B$2:B2, B2)=1, D2, 0)
求和:=SUM(辅助列)
4. 按颜色求和(需VBA)
Function SumByColor(rColor As Range, rRange As Range) As Double
Dim cell As Range
Dim total As Double
total = 0
For Each cell In rRange
If cell.Interior.Color = rColor.Interior.Color Then
total = total + cell.Value
End If
Next cell
SumByColor = total
End Function
' 使用:=SumByColor(A1, B:B) # A1是参考颜色单元格
性能优化:
# 1. 限定范围而非整列
# 慢
=SUMIF(B:B, "北京", D:D)
# 快
=SUMIF(B2:B1000, "北京", D2:D1000)
# 2. 使用SUMPRODUCT替代多个SUMIF(大数据)
=SUMPRODUCT((B2:B1000="北京")*(D2:D1000))
# 3. 复杂条件用数据透视表或辅助列
常见错误:
❌ =SUMIF(B:B, 北京, D:D) # 文本条件未加引号
✅ =SUMIF(B:B, "北京", D:D)
❌ =SUMIF(B:B, ">1000", D:D) # 数字条件加了引号仍可能出错
✅ =SUMIF(B:B, ">1000", D:D) 或 =SUMIF(B:B, ">"&1000, D:D)
❌ =SUMIFS(B:B, "北京", D:D) # SUMIFS参数顺序错误
✅ =SUMIFS(D:D, B:B, "北京")
❌ =SUMIF(B:B, F2, D:D) 但F2是数字 # 类型不匹配
✅ 确保F2与B列数据类型一致,或使用TEXT/VALUE转换
Excel数据分析技巧
▶ 如何使用数据透视表进行多维分析?
数据透视表是Excel最强大的数据分析工具,可以快速汇总和分析大量数据。
创建数据透视表:
- 选中数据区域(包含标题行)
- 插入 → 数据透视表
- 选择放置位置(新工作表/现有工作表)
- 拖拽字段到四个区域
四个区域作用:
- 筛选器:页面级筛选
- 列:水平分类
- 行:垂直分类
- 值:汇总计算
实战场景:
场景1:销售额基础汇总
数据结构:
日期 | 地区 | 产品 | 销售员 | 销售额 | 成本
步骤:
- 创建数据透视表
- 将"地区"拖到行
- 将"销售额"拖到值
- 右键"销售额" → 值字段设置 → 选择"求和"
结果:
地区 销售额总计
北京 150000
上海 200000
广州 180000
总计 530000
场景2:多维交叉分析
配置:
- 行:地区
- 列:产品类别
- 值:销售额(求和)
- 筛选器:日期(按月/季度)
结果矩阵:
手机 电脑 平板 总计
北京 50000 60000 40000 150000
上海 70000 80000 50000 200000
广州 60000 70000 50000 180000
总计 180000 210000 140000 530000
场景3:计算字段
需求:计算利润和利润率
步骤:
- 在数据透视表中右键
- 字段、项目和集 → 计算字段
- 创建新字段"利润" = 销售额 - 成本
- 创建"利润率" = 利润 / 销售额
场景4:添加切片器
步骤:
- 选中数据透视表
- 数据透视表分析 → 插入切片器
- 选择"地区"、“产品"等字段
- 点击切片器按钮快速筛选
优势:可视化筛选,支持多选,可跨表联动
值汇总方式:
右键值字段 → 值字段设置:
- 求和:总销售额
- 计数:订单数量
- 平均值:平均订单金额
- 最大值/最小值:峰值
- 标准偏差:波动性
- 百分比:
- 占总计的百分比
- 占列/行汇总的百分比
- 占父项的百分比
显示方式:
值显示方式选项:
- 差异:与基准的差值
- % 差异:与基准的百分比差异
- 累计:累计求和
- 排名:从大到小排序
- 占比:占总计/小计的百分比
分组功能:
1. 日期分组: 右键日期字段 → 组合:
- 按月/季度/年
- 自定义起止日期
2. 数值分组: 右键数值字段 → 组合:
- 设置起始值、终止值、步长
- 例:年龄分组(0-20, 20-40, 40-60, 60+)
3. 文本分组: 手动选择多个项 → 右键 → 组合
- 例:将"北京"、“上海”、“广州"组合为"一线城市”
条件格式:
在数据透视表中应用条件格式:
- 选中数值区域
- 条件格式 → 色阶/数据条/图标集
常用场景:
- 色阶:热力图显示销售分布
- 数据条:可视化对比大小
- 图标集:标记增长趋势(↑↓)
高级技巧:
1. 父子项展开:
- 双击任意汇总值 → 自动生成明细表
- 展示该组的原始记录
2. 获取数据: 使用GETPIVOTDATA函数:
=GETPIVOTDATA("销售额", $A$3, "地区", "北京", "产品", "手机")
# 从A3位置的透视表提取北京地区手机的销售额
3. 多数据源合并:
- 数据 → 获取数据 → 合并查询
- 或使用Power Query整合多表
4. 数据模型:
- 数据 → 数据模型 → 添加到数据模型
- 支持多表关联,类似SQL JOIN
- 可创建DAX计算列
5. 时间智能: 添加计算字段:
# 同比增长
=(销售额 - CALCULATE(销售额, DATEADD(日期, -1, YEAR))) / CALCULATE(销售额, DATEADD(日期, -1, YEAR))
# 环比增长(需Power Pivot)
=(销售额 - CALCULATE(销售额, DATEADD(日期, -1, MONTH))) / CALCULATE(销售额, DATEADD(日期, -1, MONTH))
实战案例:销售仪表板:
配置多个数据透视表:
1. 总览表:
- 行:无
- 列:无
- 值:总销售额、订单数、平均订单
2. 趋势表:
- 行:日期(按月)
- 列:无
- 值:销售额
- 显示:累计
3. 排行榜:
- 行:销售员
- 列:无
- 值:销售额
- 排序:降序
- 显示:Top 10
4. 区域分布:
- 行:地区
- 列:产品类别
- 值:销售额
- 格式:占比
5. 客户分析:
- 行:客户等级(RFM分组)
- 列:无
- 值:客户数、平均消费
- 图表:饼图
性能优化:
- 限制数据源大小:过滤无用记录
- 避免过多计算字段:预处理后再透视
- 使用数据模型:大数据集启用数据模型
- 定期刷新:右键 → 刷新,或设置自动刷新
- 禁用自动格式:数据透视表选项 → 取消"自动调整列宽"
常见问题:
❌ 数据源有空行/空列 → 清理后重建 ❌ 日期无法分组 → 检查格式,统一为日期类型 ❌ 求和显示为计数 → 检查数值列是否包含文本 ❌ 刷新后格式丢失 → 设置"更新时保留格式" ❌ 切片器不联动 → 右键切片器 → 报表连接 → 勾选目标透视表
▶ 如何使用条件格式实现数据可视化?
条件格式可以根据单元格值自动应用格式,直观展示数据特征。
基础条件格式:
路径:开始 → 条件格式
常用类型:
- 突出显示单元格规则
- 项目选取规则
- 数据条
- 色阶
- 图标集
实战场景:
场景1:数值高亮
目标:标记销售额>10000的单元格为绿色
步骤:
- 选中数据区域(B2:B100)
- 条件格式 → 突出显示单元格规则 → 大于
- 输入 10000
- 选择格式(绿填充色)
进阶:分段高亮
销售额 >= 15000 → 深绿色
销售额 >= 10000 → 浅绿色
销售额 >= 5000 → 黄色
销售额 < 5000 → 红色
场景2:Top N标记
目标:标记前10名销售员
步骤:
- 选中销售额列
- 条件格式 → 项目选取规则 → 前10项
- 输入 10
- 选择格式(金色填充)
或:最后10项(后10名) 或:高于/低于平均值
场景3:趋势可视化
数据条:
- 选中数值列
- 条件格式 → 数据条 → 渐变填充/实心填充
- 自动在单元格中显示横向条形图
色阶:
- 2色色阶:低值→高值(如红→绿)
- 3色色阶:低→中→高(如红→黄→绿)
图标集:
- 3个图标:箭头(↑↓)、交通灯(●)、旗标
- 4个图标:评级
- 5个图标:星级评分
场景4:自定义公式规则
隔行填色:
选中区域:A2:E100
条件格式 → 新建规则 → 使用公式确定格式
公式:=MOD(ROW(),2)=0
格式:浅灰色填充
标记重复值:
公式:=COUNTIF($A$2:$A$100, A2)>1
格式:红色填充
标记周末日期:
公式:=OR(WEEKDAY(A2)=1, WEEKDAY(A2)=7)
格式:浅蓝色填充
标记空值:
公式:=ISBLANK(A2)
格式:黄色填充+红色字体
标记超期任务(日期列):
公式:=AND(A2<TODAY(), B2="未完成")
格式:红色填充
多条件综合:
# 高优先级且超期
公式:=AND(C2="高", A2<TODAY(), B2<>"已完成")
格式:深红色填充+白色粗体
高级应用:
1. 甘特图效果:
数据结构:
A列: 任务 B列: 开始日期 C列: 完成日期
步骤:
- 在D列开始创建日期序列(每列一天)
- 选中日期区域
- 条件格式 → 新建规则 → 使用公式
- 公式:
=AND(D$1>=$B2, D$1<=$C2) - 格式:蓝色填充
结果:自动根据日期范围填充单元格
2. 热力图:
场景:销售员×产品的销售矩阵
步骤:
- 选中数值区域
- 条件格式 → 色阶 → 更多规则
- 设置最小值(红)、中间值(黄)、最大值(绿)
- 类型:百分位数或数值
3. 进度条:
公式版进度条:
# A列:实际完成 B列:目标 C列:进度条
C2公式:=REPT("█", INT(A2/B2*10))&REPT("▁", 10-INT(A2/B2*10))
条件格式版:
- 在单元格显示完成百分比
- 条件格式 → 数据条(仅显示条)
4. 到期提醒:
# 未来7天到期:橙色
=AND(A2>=TODAY(), A2<=TODAY()+7)
# 已过期:红色
=AND(A2<TODAY(), B2<>"已完成")
# 今天到期:黄色闪烁
=A2=TODAY()
5. 动态范围高亮:
需求:根据下拉选择高亮整行
步骤:
- 在F1创建下拉列表(数据验证)
- 选中数据区域A2:E100
- 条件格式 → 新建规则
- 公式:
=$B2=$F$1(B列为匹配列) - 格式:黄色填充
6. 重复值分析:
唯一值:
=COUNTIF($A$2:$A$100, A2)=1
格式:绿色
首次出现:
=COUNTIF($A$2:A2, A2)=1
格式:蓝色
重复值:
=COUNTIF($A$2:$A$100, A2)>1
格式:红色
7. 对比列差异:
# 标记A列和B列不同的行
=$A2<>$B2
格式:黄色背景
8. 数据质量检查:
包含特殊字符:
=ISNUMBER(SEARCH("*", A2))+ISNUMBER(SEARCH("?", A2))>0
长度异常(如手机号):
=LEN(A2)<>11
包含空格:
=LEN(A2)<>LEN(TRIM(A2))
管理条件格式:
查看所有规则:
- 条件格式 → 管理规则
- 显示:当前选定区域/整个工作表
规则优先级:
- 上面的规则优先
- 可拖动调整顺序
停止后续规则:
- 勾选"如果为真则停止"
- 防止规则冲突
清除格式:
- 条件格式 → 清除规则 → 清除所选单元格/整张表
性能优化:
- 限定范围:不要整列应用(A:A → A2:A1000)
- 减少公式规则:能用内置规则就不用公式
- 简化公式:避免SUMIF、VLOOKUP等复杂函数
- 合并规则:多个相似规则用一个+公式判断
常见问题:
❌ 规则不生效 → 检查单元格是否有其他格式覆盖 ❌ 复制后格式不跟随 → 选择性粘贴 → 格式 ❌ 公式引用错误 → 注意绝对引用($)使用 ❌ 规则过多导致卡顿 → 清理无用规则,简化公式
▶ 如何使用Power Query进行数据清洗?
Power Query是Excel强大的数据整合与清洗工具,可自动化处理重复性数据任务。
启动Power Query:
数据 → 获取数据 → 从文件/其他来源
或: 数据 → 从表格/区域(将当前数据加载到Power Query)
Power Query编辑器:
主要功能区:
- 主页:常用转换操作
- 转换:数据类型、拆分、合并
- 添加列:基于现有列计算
- 视图:查询设置、公式栏
实战场景:
场景1:基础数据清洗
常见问题:
- 首行非标题
- 数据类型错误
- 包含空行/空列
- 有多余空格
步骤:
- 加载数据到Power Query
- 首行提升为标题:主页 → 将第一行用作标题
- 删除空行:主页 → 删除行 → 删除空行
- 删除列:选中列 → 右键 → 删除
- 修剪空格:选中文本列 → 转换 → 格式 → 修剪
- 更改数据类型:选中列 → 右键 → 更改类型
示例:清洗客户数据
原始:
姓名 年龄 电话
张三 25 1380000****
李四 30 1390000****
(空行)
王五 abc 1400000****
清洗后:
姓名 年龄 电话
张三 25 13800000000
李四 30 13900000000
王五 (null) 14000000000
步骤:
1. 删除空行
2. 修剪空格
3. 年龄列:类型改为整数,错误替换为null
4. 电话列:替换值"****"→完整号码(使用替换值功能)
场景2:拆分与合并列
拆分列:
示例:姓名列拆分为姓和名
原始:张三
目标:姓=张,名=三
步骤:
1. 选中"姓名"列
2. 转换 → 拆分列 → 按字符数
3. 输入 1(第一个字符后拆分)
4. 重命名列:姓、名
按分隔符拆分:
原始:北京-朝阳区
步骤:
1. 选中列
2. 拆分列 → 按分隔符 → 选择"-"
3. 拆分选项:在每次出现分隔符时
合并列:
示例:合并省市区
原始:省=北京,市=北京市,区=朝阳区
目标:完整地址=北京北京市朝阳区
步骤:
1. 选中"省"、"市"、"区"三列(按Ctrl多选)
2. 转换 → 合并列
3. 分隔符:无(或自定义如"-")
4. 新列名:完整地址
场景3:合并多个文件
需求:合并文件夹中所有Excel文件
步骤:
- 数据 → 获取数据 → 从文件 → 从文件夹
- 选择包含Excel文件的文件夹
- 点击"合并和加载" → 合并工作表
- 选择要合并的工作表(如Sheet1)
- Power Query自动识别列
- 删除不需要的元数据列(如"Source.Name")
- 关闭并加载
进阶:追加不同结构的表
表1:姓名、年龄、部门
表2:姓名、年龄、城市
步骤:
1. 分别加载两表
2. 主页 → 追加查询 → 三个或更多表
3. 选择表1、表2
4. 结果:共有列自动匹配,独有列保留(其他表此列为null)
场景4:分组聚合
需求:按部门统计平均工资和人数
步骤:
- 加载员工表
- 转换 → 分组依据
- 分组列:部门
- 新列名"平均工资":操作=平均值,列=工资
- 添加聚合"人数":操作=对行计数
- 确定
结果:
部门 平均工资 人数
销售部 8500 10
技术部 12000 15
行政部 7000 5
进阶:多列分组+多聚合
分组:部门、城市
聚合:
- 工资总和
- 最高工资
- 最低工资
- 员工数
高级功能:
1. 条件列:
添加列 → 条件列
示例:根据年龄分类
如果 [年龄] < 30 则 "年轻"
否则如果 [年龄] < 50 则 "中年"
否则 "资深"
2. 自定义列(M语言):
添加列 → 自定义列
示例:计算年终奖
if [销售额] >= 100000 then [销售额] * 0.1
else if [销售额] >= 50000 then [销售额] * 0.05
else 0
3. 数据透视/逆透视:
逆透视(宽表→长表):
原始:
姓名 Q1 Q2 Q3 Q4
张三 100 120 110 130
逆透视后:
姓名 季度 销售额
张三 Q1 100
张三 Q2 120
张三 Q3 110
张三 Q4 130
步骤:
1. 选中"姓名"列
2. 转换 → 逆透视其他列
3. 重命名"属性"列为"季度","值"列为"销售额"
透视列(长表→宽表):
转换 → 透视列
值列:选择要透视的列
值:选择聚合方式
高级选项:值聚合函数
4. 合并查询(类似VLOOKUP):
示例:订单表关联客户表
步骤:
- 加载订单表和客户表到Power Query
- 在订单表中:主页 → 合并查询
- 选择客户表
- 匹配列:订单表的"客户ID" = 客户表的"客户ID"
- 连接类型:左外部(保留订单表所有行)
- 确定后,展开新列,选择需要的字段
连接类型:
- 左外部:保留左表所有行(=SQL LEFT JOIN)
- 右外部:保留右表所有行
- 完全外部:保留两表所有行(=FULL OUTER JOIN)
- 内部:只保留匹配行(=INNER JOIN)
- 左反:只保留左表独有行
- 右反:只保留右表独有行
5. 参数化查询:
创建动态筛选条件
步骤:
- 主页 → 管理参数 → 新建参数
- 名称:StartDate,类型:日期,当前值:2024-01-01
- 在查询中引用参数:
- 筛选日期列 → 自定义筛选器
- 大于或等于 → 参数 → StartDate
6. 数据类型智能识别:
自动检测:
- 日期:2024-01-01 → 日期
- 数字:123 → 整数
- 文本:ABC → 文本
手动修改:
- 选中列 → 转换 → 数据类型 → 选择类型
常见类型:
- 整数、十进制数、货币
- 日期、时间、日期时间
- 文本、True/False
实战案例:销售数据清洗流程:
原始数据问题:
- 多个月份Excel文件
- 列名不统一
- 包含汇总行
- 日期格式混乱
- 金额包含"¥“符号
完整步骤:
- 合并文件:从文件夹加载所有文件
- 删除汇总行:筛选器 → 删除包含"合计"的行
- 标准化列名:重命名列确保一致
- 清理金额列:
- 替换值:"¥” → ""
- 替换值:"," → ""
- 更改类型:十进制数
- 标准化日期:
- 更改类型:日期
- 错误处理:替换错误值为null或删除行
- 添加计算列:
- 月份:
Date.Month([日期]) - 季度:
Date.QuarterOfYear([日期]) - 星期几:
Date.DayOfWeek([日期])
- 月份:
- 数据验证:
- 检查重复:分组依据 → 对行计数
- 统计缺失:添加条件列标记空值
- 关闭并加载:加载到Excel或数据模型
性能优化:
- 仅加载需要的列:删除无用列
- 提前筛选:在Power Query中过滤,减少数据量
- 禁用类型检测:大文件可禁用自动检测类型
- 查询折叠:利用数据源性能(SQL、数据库)
- 避免复杂M语言:优先使用内置功能
刷新数据:
Power Query可保存清洗步骤,数据更新后一键刷新:
- 右键查询 → 刷新
- 或:数据 → 全部刷新
常见问题:
❌ 步骤错误 → 在"应用的步骤"中删除或修改错误步骤 ❌ 列类型自动转换错误 → 手动更改类型并将错误替换为null ❌ 合并查询无结果 → 检查匹配列数据类型是否一致 ❌ M语言报错 → 检查语法,参考公式栏示例
▶ 如何使用INDEX+MATCH替代VLOOKUP?
INDEX+MATCH组合比VLOOKUP更灵活强大,支持左侧查找和动态列。
基础语法:
=INDEX(返回范围, MATCH(查找值, 查找范围, 0))
示例:
=INDEX($E$2:$E$10, MATCH(A2, $D$2:$D$10, 0))
与VLOOKUP对比:
| 特性 | VLOOKUP | INDEX+MATCH |
|---|---|---|
| 查找方向 | 只能向右查找 | 任意方向 |
| 插入列影响 | 需要修改列号 | 不受影响 |
| 性能 | 较慢(扫描整行) | 较快(只查指定列) |
| 灵活性 | 低 | 高 |
实战案例:
场景1:向左查找(VLOOKUP做不到)
表格结构:
A列: 员工姓名 B列: 员工编号(要查找)
C列: 查找编号 D列: 返回姓名
公式:
=INDEX($A$2:$A$100, MATCH(C2, $B$2:$B$100, 0))
说明:根据编号(在右侧B列)查找姓名(在左侧A列)
场景2:双向查找(行列交叉)
表格结构(成绩表):
A B C D
1 语文 数学 英语
2 张三 85 90 88
3 李四 92 87 91
查找张三的数学成绩:
=INDEX($B$2:$D$3,
MATCH("张三", $A$2:$A$3, 0), # 行号
MATCH("数学", $B$1:$D$1, 0)) # 列号
可变公式(查找单元格F2中的学生,G2中的科目):
=INDEX($B$2:$D$3,
MATCH(F2, $A$2:$A$3, 0),
MATCH(G2, $B$1:$D$1, 0))
场景3:多条件查找
需求:根据部门+职位查找薪资
表格结构:
A列: 部门 B列: 职位 C列: 薪资
辅助列法(在D列创建辅助列):
# D2单元格:
=A2&"-"&B2
# E2查找公式:
=INDEX($C$2:$C$100,
MATCH(F2&"-"&G2, $D$2:$D$100, 0))
数组公式法(不需辅助列,Ctrl+Shift+Enter):
=INDEX($C$2:$C$100,
MATCH(1, ($A$2:$A$100=F2)*($B$2:$B$100=G2), 0))
或使用SUMIFS简化多条件:
=SUMIFS($C$2:$C$100,
$A$2:$A$100, F2,
$B$2:$B$100, G2)
场景4:动态范围(OFFSET结合)
需求:查找最后一次出现的值
# 查找最后一个非空值
=INDEX($A$2:$A$100,
COUNTA($A$2:$A$100))
# 查找最后一个匹配项
=INDEX($B$2:$B$100,
MAX(IF($A$2:$A$100=F2, ROW($A$2:$A$100)-ROW($A$2)+1)))
# 数组公式,需Ctrl+Shift+Enter
进阶技巧:
1. 近似匹配(有序数据)
# 查找小于等于查找值的最大值(成绩等级)
=INDEX($B$2:$B$6,
MATCH(A2, $A$2:$A$6, 1)) # 1表示近似匹配
示例数据:
A列(分数阈值) B列(等级)
90 优秀
80 良好
70 中等
60 及格
0 不及格
2. 查找部分匹配(通配符)
# 模糊查找包含"北京"的项
=INDEX($B$2:$B$100,
MATCH("*北京*", $A$2:$A$100, 0))
3. 区分大小写查找
# 普通MATCH不区分大小写,使用EXACT数组公式
=INDEX($B$2:$B$100,
MATCH(TRUE, EXACT($A$2:$A$100, F2), 0))
4. 错误处理
# 使用IFERROR防止#N/A错误
=IFERROR(
INDEX($B$2:$B$100, MATCH(A2, $A$2:$A$100, 0)),
"未找到"
)
# 或使用IFNA(仅捕获#N/A错误)
=IFNA(
INDEX($B$2:$B$100, MATCH(A2, $A$2:$A$100, 0)),
"未找到"
)
性能优化:
- 固定查找范围:使用绝对引用($)固定范围
- 减小查找区域:只查找必要的列,不查整行
- 避免嵌套过多:复杂情况考虑辅助列
- 使用命名区域:提高可读性和性能
# 定义名称:员工列表 = $A$2:$A$100
# 公式更清晰:
=INDEX(员工列表, MATCH(F2, 编号列表, 0))
▶ 如何使用COUNTIF/COUNTIFS进行条件计数?
条件计数函数用于统计满足特定条件的单元格数量。
COUNTIF语法:
=COUNTIF(范围, 条件)
示例:
=COUNTIF(A2:A100, ">80") # 大于80的数量
=COUNTIF(B2:B100, "北京") # 等于"北京"的数量
=COUNTIF(C2:C100, "张*") # 以"张"开头的数量
COUNTIFS语法(多条件):
=COUNTIFS(条件范围1, 条件1, 条件范围2, 条件2, ...)
示例:
=COUNTIFS($A$2:$A$100, ">=80", $A$2:$A$100, "<90") # 80-90之间
=COUNTIFS($B$2:$B$100, "北京", $C$2:$C$100, "销售") # 北京且销售部门
常用条件写法:
# 数值条件
">80" # 大于80
">=60" # 大于等于60
"<>0" # 不等于0
"="&A1 # 等于单元格A1的值
">="&A1 # 大于等于A1
# 文本条件
"张三" # 完全匹配
"张*" # 以"张"开头(*表示任意字符)
"*有限公司" # 以"有限公司"结尾
"*北京*" # 包含"北京"
"???" # 恰好3个字符(?表示单个字符)
# 日期条件
">="&DATE(2024,1,1) # 2024年1月1日及之后
">"&TODAY()-7 # 最近7天
">="&EOMONTH(TODAY(),-1)+1 # 本月
实战案例:
场景1:销售数据统计
数据结构:
A列: 销售额 B列: 区域 C列: 销售员
统计公式:
# 销售额大于10000的订单数
=COUNTIF($A$2:$A$100, ">10000")
# 北京区域的订单数
=COUNTIF($B$2:$B$100, "北京")
# 北京区域且销售额>10000的订单数
=COUNTIFS($B$2:$B$100, "北京", $A$2:$A$100, ">10000")
# 张三在北京的订单数
=COUNTIFS($B$2:$B$100, "北京", $C$2:$C$100, "张三")
# 统计张三或李四的订单数(使用SUM+COUNTIF)
=COUNTIF($C$2:$C$100, "张三") + COUNTIF($C$2:$C$100, "李四")
场景2:成绩分段统计
# 优秀(>=90)
=COUNTIF($A$2:$A$50, ">=90")
# 良好(80-89)
=COUNTIFS($A$2:$A$50, ">=80", $A$2:$A$50, "<90")
# 及格(60-79)
=COUNTIFS($A$2:$A$50, ">=60", $A$2:$A$50, "<80")
# 不及格(<60)
=COUNTIF($A$2:$A$50, "<60")
# 汇总表格式
| 等级 | 分数段 | 人数 |
|------|--------|------|
| 优秀 | >=90 | =COUNTIF($A$2:$A$50,">=90") |
| 良好 | 80-89 | =COUNTIFS($A$2:$A$50,">=80",$A$2:$A$50,"<90") |
| 中等 | 70-79 | =COUNTIFS($A$2:$A$50,">=70",$A$2:$A$50,"<80") |
| 及格 | 60-69 | =COUNTIFS($A$2:$A$50,">=60",$A$2:$A$50,"<70") |
| 不及格 | <60 | =COUNTIF($A$2:$A$50,"<60") |
场景3:文本模糊统计
# 统计包含"有限公司"的单位数量
=COUNTIF($A$2:$A$100, "*有限公司*")
# 统计以"138"开头的手机号数量
=COUNTIF($B$2:$B$100, "138*")
# 统计姓张的人数
=COUNTIF($C$2:$C$100, "张*")
# 统计3个字的姓名数量
=COUNTIF($C$2:$C$100, "???")
# 排除包含"测试"的记录数量
=COUNTA($A$2:$A$100) - COUNTIF($A$2:$A$100, "*测试*")
场景4:日期范围统计
# 统计本月数据量
=COUNTIFS($A$2:$A$1000, ">="&DATE(YEAR(TODAY()),MONTH(TODAY()),1),
$A$2:$A$1000, "<"&DATE(YEAR(TODAY()),MONTH(TODAY())+1,1))
# 或使用EOMONTH
=COUNTIFS($A$2:$A$1000, ">="&EOMONTH(TODAY(),-1)+1,
$A$2:$A$1000, "<="&EOMONTH(TODAY(),0))
# 统计最近7天数据量
=COUNTIF($A$2:$A$1000, ">="&TODAY()-7)
# 统计2024年数据量
=COUNTIFS($A$2:$A$1000, ">="&DATE(2024,1,1),
$A$2:$A$1000, "<="&DATE(2024,12,31))
# 统计上季度数据量
=COUNTIFS($A$2:$A$1000, ">="&DATE(YEAR(TODAY()),MONTH(TODAY())-3,1),
$A$2:$A$1000, "<"&DATE(YEAR(TODAY()),MONTH(TODAY()),1))
场景5:动态条件(引用单元格)
# 条件在单元格F2和G2中
=COUNTIFS($B$2:$B$100, F2, $C$2:$C$100, G2)
# 范围条件(最小值在F2,最大值在G2)
=COUNTIFS($A$2:$A$100, ">="&F2, $A$2:$A$100, "<="&G2)
# 动态日期范围(开始日期F2,结束日期G2)
=COUNTIFS($A$2:$A$1000, ">="&F2, $A$2:$A$1000, "<="&G2)
# 多条件OR逻辑(部门是F2或F3)
=COUNTIF($B$2:$B$100, F2) + COUNTIF($B$2:$B$100, F3)
# 使用下拉列表的动态条件
# 在F2设置数据验证(列表:全部,北京,上海,广州)
=IF(F2="全部",
COUNTA($B$2:$B$100),
COUNTIF($B$2:$B$100, F2))
进阶技巧:
1. 统计唯一值数量
# 使用SUMPRODUCT统计唯一值
=SUMPRODUCT(1/COUNTIF($A$2:$A$100,$A$2:$A$100))
# 去除空值的唯一值统计
=SUMPRODUCT(($A$2:$A$100<>"")/COUNTIF($A$2:$A$100,$A$2:$A$100&""))
2. 统计不重复的条件值
# 统计北京有多少个不同的销售员
=SUMPRODUCT(($B$2:$B$100="北京")/COUNTIFS($C$2:$C$100,$C$2:$C$100,$B$2:$B$100,"北京"))
3. 复杂OR条件
# 统计部门为销售或市场的数量
=COUNTIF($A$2:$A$100,"销售") + COUNTIF($A$2:$A$100,"市场")
# 使用SUMPRODUCT实现多OR条件
=SUMPRODUCT((($A$2:$A$100="销售")+($A$2:$A$100="市场"))>0)
4. 排除多个条件
# 统计不是北京也不是上海的数量
=COUNTA($B$2:$B$100)
- COUNTIF($B$2:$B$100,"北京")
- COUNTIF($B$2:$B$100,"上海")
# 或使用SUMPRODUCT
=SUMPRODUCT(($B$2:$B$100<>"北京")*($B$2:$B$100<>"上海"))
▶ 如何使用数组公式进行批量计算?
数组公式可以同时对多个值进行计算,是Excel的高级功能。
数组公式基础:
- 输入方式:编写公式后按
Ctrl+Shift+Enter - 显示特征:公式两侧会显示花括号
{}(不能手动输入) - Excel 365:动态数组会自动溢出,不需要Ctrl+Shift+Enter
基础示例:
# 传统方法:逐行计算
A1*B1, A2*B2, A3*B3, ...
# 数组公式:一次计算整列
{=A1:A10*B1:B10}
# 结果会自动填充到10行
实战案例:
场景1:多条件求和(不使用SUMIFS)
数据结构:
A列: 部门 B列: 产品 C列: 销售额
公式(查找销售部门+产品A的总额):
{=SUM((A2:A100="销售")*(B2:B100="产品A")*(C2:C100))}
解释:
(A2:A100="销售")返回 TRUE/FALSE 数组(B2:B100="产品A")返回 TRUE/FALSE 数组- 相乘后 TRUE=1, FALSE=0
- 最后与销售额相乘并求和
等价的SUMIFS写法:
=SUMIFS(C2:C100, A2:A100, "销售", B2:B100, "产品A")
场景2:提取唯一值列表(Excel 2019前)
# 提取A列的唯一值到B列
# B1公式:
{=IFERROR(INDEX($A$2:$A$100,
MATCH(0, COUNTIF($B$1:B1, $A$2:$A$100), 0)), "")}
# 向下拖动B2, B3, ...
Excel 365更简单的方法:
=UNIQUE(A2:A100)
# 自动返回唯一值数组
场景3:动态提取Top N
提取前3名的销售额及对应销售员:
# 提取第N大的值(N在E列)
# F1公式(提取销售额):
{=LARGE($C$2:$C$100, E1)}
# G1公式(提取对应销售员):
{=INDEX($A$2:$A$100,
MATCH(F1, $C$2:$C$100, 0))}
# 或使用数组公式一次完成:
{=INDEX($A$2:$A$100,
MATCH(LARGE($C$2:$C$100, ROW(A1)),
$C$2:$C$100, 0))}
# 向下拖动可得Top1, Top2, Top3...
Excel 365方法:
=SORT(A2:C100, 3, -1) # 按第3列降序排序
# 或
=FILTER(A2:B100, C2:C100>=LARGE(C2:C100,3)) # 筛选前3名
场景4:多条件查找(返回多个结果)
需求:查找所有属于"销售部"的员工姓名
传统数组公式:
# 第一个结果
{=INDEX($A$2:$A$100,
SMALL(IF($B$2:$B$100="销售部", ROW($A$2:$A$100)-ROW($A$2)+1),
ROW(A1)))}
# 向下拖动得到第2、3、4...个结果
# 当没有更多结果时显示错误,用IFERROR包裹
{=IFERROR(INDEX($A$2:$A$100,
SMALL(IF($B$2:$B$100="销售部", ROW($A$2:$A$100)-ROW($A$2)+1),
ROW(A1))), "")}
Excel 365动态数组:
=FILTER(A2:A100, B2:B100="销售部")
# 自动返回所有匹配结果
常用数组函数:
1. 统计函数
# 统计满足多条件的数量
{=SUM((A2:A100="条件1")*(B2:B100="条件2"))}
# 计算加权平均
{=SUM(A2:A10*B2:B10)/SUM(B2:B10)}
# A列是数值,B列是权重
# 统计唯一值数量
{=SUM(1/COUNTIF(A2:A100,A2:A100))}
2. 查找函数
# 返回最后一个匹配项
{=INDEX(B2:B100,
MAX(IF(A2:A100="查找值", ROW(A2:A100)-ROW(A2)+1)))}
# 多条件查找
{=INDEX(C2:C100,
MATCH(1, (A2:A100="条件1")*(B2:B100="条件2"), 0))}
3. 文本处理
# 批量提取文本(如提取所有单元格的前3个字符)
{=LEFT(A2:A10, 3)}
# Excel 365自动溢出
# 批量连接文本
{=A2:A10&" - "&B2:B10}
4. 条件计算
# 根据条件返回不同值
{=IF(A2:A10>100, "高", IF(A2:A10>50, "中", "低"))}
# Excel 365自动溢出
# 批量舍入
{=ROUND(A2:A10, 2)}
Excel 365动态数组函数:
FILTER - 筛选
# 基础筛选
=FILTER(A2:C100, B2:B100="销售")
# 多条件筛选(AND)
=FILTER(A2:C100, (B2:B100="销售")*(C2:C100>10000))
# 多条件筛选(OR)
=FILTER(A2:C100, (B2:B100="销售")+(B2:B100="市场"))
# 未找到时返回默认值
=FILTER(A2:C100, B2:B100="XX", "未找到匹配项")
SORT - 排序
# 按第3列降序排序
=SORT(A2:C100, 3, -1)
# 多列排序(先按第2列升序,再按第3列降序)
=SORT(A2:C100, {2,3}, {1,-1})
SORTBY - 按其他列排序
# 按销售额排序员工表
=SORTBY(A2:B100, C2:C100, -1)
# A:B是姓名和部门,C是销售额
UNIQUE - 去重
# 提取唯一值
=UNIQUE(A2:A100)
# 提取多列唯一组合
=UNIQUE(A2:C100)
# 只返回出现一次的值
=UNIQUE(A2:A100, FALSE, TRUE)
SEQUENCE - 生成序列
# 生成1到10
=SEQUENCE(10)
# 生成3行4列矩阵
=SEQUENCE(3, 4)
# 生成10到100,步长10
=SEQUENCE(10, 1, 10, 10)
数组公式调试技巧:
- F9键调试:选中公式的一部分,按F9查看计算结果
- FORMULATEXT查看公式:
=FORMULATEXT(A1)显示A1的公式文本 - 分步构建:先写简单公式,逐步添加条件
- 使用辅助列:复杂数组公式先用辅助列验证逻辑
性能注意事项:
- ❌ 避免在大数据集上使用易失性函数(INDIRECT、OFFSET)
- ✅ 优先使用SUMIFS、COUNTIFS等内置函数
- ✅ 将数组公式应用范围限制在必要范围
- ✅ Excel 365的FILTER/SORT比传统数组公式性能更好
▶ 如何使用TEXT和格式化函数处理数据?
TEXT函数和格式化功能可以将数值、日期等转换为特定格式的文本。
TEXT函数基础:
=TEXT(值, 格式代码)
示例:
=TEXT(12345, "0.00") # 12345.00
=TEXT(0.85, "0%") # 85%
=TEXT(TODAY(), "yyyy-mm-dd") # 2024-01-15
常用格式代码:
数字格式代码:
# 小数位数
=TEXT(1234.5, "0") # 1235(四舍五入)
=TEXT(1234.5, "0.0") # 1234.5
=TEXT(1234.5, "0.00") # 1234.50
=TEXT(1234.567, "0.00") # 1234.57
# 千分位分隔符
=TEXT(1234567, "#,##0") # 1,234,567
=TEXT(1234567.89, "#,##0.00") # 1,234,567.89
# 百分比
=TEXT(0.85, "0%") # 85%
=TEXT(0.8567, "0.00%") # 85.67%
# 货币符号
=TEXT(1234.5, "¥#,##0.00") # ¥1,234.50
=TEXT(1234.5, "$#,##0.00") # $1,234.50
# 科学计数法
=TEXT(1234567, "0.00E+00") # 1.23E+06
# 正负数不同格式
=TEXT(1234, "#,##0;(#,##0)") # 1,234
=TEXT(-1234, "#,##0;(#,##0)") # (1,234)
# 前导零
=TEXT(5, "000") # 005
=TEXT(42, "0000") # 0042
日期格式代码:
# 年份
=TEXT(TODAY(), "yyyy") # 2024(4位年份)
=TEXT(TODAY(), "yy") # 24(2位年份)
# 月份
=TEXT(TODAY(), "mm") # 01(2位月份)
=TEXT(TODAY(), "m") # 1(1位月份)
=TEXT(TODAY(), "mmm") # Jan(英文缩写)
=TEXT(TODAY(), "mmmm") # January(英文全称)
=TEXT(TODAY(), "mmmmm") # J(首字母)
# 日期
=TEXT(TODAY(), "dd") # 15(2位日期)
=TEXT(TODAY(), "d") # 15(1位日期)
=TEXT(TODAY(), "ddd") # Mon(星期缩写)
=TEXT(TODAY(), "dddd") # Monday(星期全称)
# 常用组合
=TEXT(TODAY(), "yyyy-mm-dd") # 2024-01-15
=TEXT(TODAY(), "yyyy/mm/dd") # 2024/01/15
=TEXT(TODAY(), "yyyy年mm月dd日") # 2024年01月15日
=TEXT(TODAY(), "m/d/yyyy") # 1/15/2024
=TEXT(TODAY(), "dd-mmm-yyyy") # 15-Jan-2024
=TEXT(TODAY(), "dddd, mmmm dd, yyyy") # Monday, January 15, 2024
时间格式代码:
# 时分秒
=TEXT(NOW(), "hh:mm:ss") # 14:30:25(24小时制)
=TEXT(NOW(), "h:mm AM/PM") # 2:30 PM(12小时制)
=TEXT(NOW(), "hh:mm") # 14:30
# 日期+时间
=TEXT(NOW(), "yyyy-mm-dd hh:mm:ss") # 2024-01-15 14:30:25
=TEXT(NOW(), "m/d/yyyy h:mm AM/PM") # 1/15/2024 2:30 PM
# 时长格式(小时数可能>24)
=TEXT(0.75, "[h]:mm") # 18:00(0.75天=18小时)
=TEXT(1.5, "[h]:mm") # 36:00(1.5天=36小时)
自定义格式(包含条件):
# 格式:正数格式;负数格式;零值格式;文本格式
=TEXT(100, "#,##0;(#,##0);零;@") # 100
=TEXT(-100, "#,##0;(#,##0);零;@") # (100)
=TEXT(0, "#,##0;(#,##0);零;@") # 零
# 不同数值范围显示不同颜色(仅单元格格式有效,TEXT不支持颜色)
# 单元格格式:[蓝色][>=100]#,##0;[红色][<0](#,##0);#,##0
# 隐藏某类值
=TEXT(100, "#,##0;#,##0;") # 零值显示为空
=TEXT(0, "#,##0;#,##0;") # 空
# 添加文字说明
=TEXT(85, "成绩:0分") # 成绩:85分
=TEXT(1.5, "0.0米") # 1.5米
实战案例:
1. 构造工号/订单号:
# 构造6位工号(前导零)
=TEXT(ROW(), "000000") # 000001, 000002, ...
# 构造订单号(日期+序号)
=TEXT(TODAY(), "yyyymmdd")&TEXT(ROW(), "0000")
# 结果:202401150001
# 带前缀的编号
="EMP-"&TEXT(A2, "00000") # EMP-00001
="ORD-"&TEXT(TODAY(), "yyyymmdd")&"-"&TEXT(A2, "000")
# 结果:ORD-20240115-001
2. 提取和组合日期部分:
# 提取年月
=TEXT(A2, "yyyy-mm") # 2024-01
# 季度标识
="Q"&TEXT(A2, "q")&"-"&TEXT(A2, "yyyy") # Q1-2024
# 中文日期
=TEXT(A2, "yyyy年m月d日") # 2024年1月15日
# 周次
="第"&WEEKNUM(A2)&"周" # 第3周
3. 数值格式化显示:
# 将小数转为百分比文本
=TEXT(A2, "0.00%") # A2=0.856 → 85.60%
# 财务格式
=TEXT(A2, "¥#,##0.00;¥-#,##0.00") # ¥12,345.67 或 ¥-12,345.67
# 保留有效数字
=TEXT(A2, "0.00E+00") # 科学计数法
4. 文本拼接:
# 构造完整句子
="销售额:"&TEXT(A2, "¥#,##0.00")&",增长率:"&TEXT(B2, "0.00%")
# 结果:销售额:¥12,345.67,增长率:15.30%
# 动态文件名
=TEXT(TODAY(), "yyyy-mm-dd")&"_销售报表.xlsx"
# 结果:2024-01-15_销售报表.xlsx
5. 条件格式化:
# 根据数值范围显示不同文本
=IF(A2>=90, "优秀:"&TEXT(A2, "0.0"),
IF(A2>=60, "及格:"&TEXT(A2, "0.0"),
"不及格:"&TEXT(A2, "0.0")))
注意事项:
TEXT返回文本:不能直接用于计算
=TEXT(100, "0")+50 # 错误!TEXT返回的是文本 =VALUE(TEXT(100, "0"))+50 # 正确:先转回数值日期格式mm vs MMM:
mm= 月份数字(01, 02)mmm= 月份缩写(Jan, Feb)
时间格式h vs [h]:
h= 小时(0-23)[h]= 总小时数(可>24)
区域设置影响:不同地区的日期月份名称可能不同
组合其他函数:
# 提取中文星期
=TEXT(A2, "aaaa") # 星期一(中文系统)
# 提取年龄(从身份证号)
=YEAR(TODAY())-VALUE(MID(A2, 7, 4)) # 身份证号在A2
# 构造SQL查询字符串
="SELECT * FROM orders WHERE date = '"&TEXT(A2, "yyyy-mm-dd")&"'"
▶ 如何使用LEFT、RIGHT、MID提取文本?
文本提取函数用于从字符串中获取指定位置和长度的字符。
基础语法:
=LEFT(文本, [字符数]) # 从左侧提取
=RIGHT(文本, [字符数]) # 从右侧提取
=MID(文本, 起始位置, 字符数) # 从中间提取
基础示例:
# 示例文本:"Excel2024数据分析"
=LEFT("Excel2024数据分析", 5) # Excel
=RIGHT("Excel2024数据分析", 4) # 数据分析
=MID("Excel2024数据分析", 6, 4) # 2024
# 提取单元格A2的内容
=LEFT(A2, 3) # 前3个字符
=RIGHT(A2, 5) # 后5个字符
=MID(A2, 4, 6) # 从第4个字符开始提取6个
实战案例:
场景1:身份证号信息提取
假设身份证号在A列:110101199001011234
# 提取省份代码(前2位)
=LEFT(A2, 2) # 11
# 提取市代码(3-4位)
=MID(A2, 3, 2) # 01
# 提取区代码(5-6位)
=MID(A2, 5, 2) # 01
# 提取出生年份(7-10位)
=MID(A2, 7, 4) # 1990
# 提取出生月份(11-12位)
=MID(A2, 11, 2) # 01
# 提取出生日期(13-14位)
=MID(A2, 13, 2) # 01
# 完整出生日期
=TEXT(DATE(MID(A2,7,4), MID(A2,11,2), MID(A2,13,2)), "yyyy-mm-dd")
# 1990-01-01
# 性别判断(倒数第2位,奇数=男,偶数=女)
=IF(MOD(MID(A2, 17, 1), 2)=1, "男", "女")
# 年龄计算
=YEAR(TODAY())-MID(A2, 7, 4)
场景2:手机号脱敏处理
手机号:13812345678
# 中间4位脱敏
=LEFT(A2, 3)&"****"&RIGHT(A2, 4)
# 结果:138****5678
# 保留前3后2
=LEFT(A2, 3)&"******"&RIGHT(A2, 2)
# 结果:138******78
# 分段显示
=LEFT(A2, 3)&"-"&MID(A2, 4, 4)&"-"&RIGHT(A2, 4)
# 结果:138-1234-5678
场景3:地址拆分
地址:北京市朝阳区建国路100号
# 提取省/直辖市(前2-3个字符)
# 方法1:固定长度
=LEFT(A2, 3) # 北京市
# 方法2:查找"省"或"市"
=LEFT(A2, FIND("市", A2)) # 北京市
# 提取区县(查找第二个"市"到"区"之间)
=MID(A2, FIND("市", A2)+1, FIND("区", A2)-FIND("市", A2))
# 朝阳区
# 提取街道+门牌号("区"之后)
=MID(A2, FIND("区", A2)+1, LEN(A2))
# 建国路100号
# 提取门牌号数字
=LEFT(MID(A2, FIND("路", A2)+1, 10),
FIND("号", MID(A2, FIND("路", A2)+1, 10))-1)
# 100
场景4:文件名处理
文件名:2024-01-15_销售报表_V1.2.xlsx
# 提取日期部分
=LEFT(A2, 10) # 2024-01-15
# 提取文件主名(去除扩展名)
=LEFT(A2, FIND(".", A2)-1)
# 2024-01-15_销售报表_V1.2
# 提取扩展名
=RIGHT(A2, LEN(A2)-FIND(".", A2))
# xlsx
# 或
=MID(A2, FIND(".", A2)+1, LEN(A2))
# 提取版本号
=MID(A2, FIND("_V", A2)+2, FIND(".", A2, FIND("_V", A2))-FIND("_V", A2)-2)
# 1.2
# 更复杂:按"_"拆分提取第2部分
=MID(A2, FIND("_", A2)+1,
FIND("_", A2, FIND("_", A2)+1)-FIND("_", A2)-1)
# 销售报表
场景5:产品编号拆分
编号:PRD-2024-001-BJ
# 提取前缀
=LEFT(A2, 3) # PRD
# 提取年份
=MID(A2, 5, 4) # 2024
# 提取序号
=MID(A2, 10, 3) # 001
# 提取地区代码
=RIGHT(A2, 2) # BJ
# 或使用分隔符"-"查找
=LEFT(A2, FIND("-", A2)-1) # PRD(第一部分)
# 提取第2部分(第1个"-"到第2个"-"之间)
=MID(A2, FIND("-", A2)+1,
FIND("-", A2, FIND("-", A2)+1)-FIND("-", A2)-1)
# 2024
组合函数使用:
1. 与FIND/SEARCH结合(动态位置):
# 提取@前面的用户名(邮箱)
=LEFT(A2, FIND("@", A2)-1)
# alice@gmail.com → alice
# 提取@后面的域名
=MID(A2, FIND("@", A2)+1, LEN(A2))
# alice@gmail.com → gmail.com
# 提取最后一个空格前的内容
=LEFT(A2, FIND("~", SUBSTITUTE(A2, " ", "~", LEN(A2)-LEN(SUBSTITUTE(A2, " ", ""))))-1)
2. 与LEN结合(相对长度):
# 提取后半部分
=RIGHT(A2, LEN(A2)/2)
# 提取中间部分(去掉首尾各2个字符)
=MID(A2, 3, LEN(A2)-4)
3. 数值提取:
# 从文本"价格:¥1234.50"中提取数字
=VALUE(MID(A2, FIND("¥", A2)+1, LEN(A2)))
# 或
=--MID(A2, FIND("¥", A2)+1, LEN(A2)) # --是将文本转数字的技巧
# 提取字符串中的所有数字(数组公式)
{=SUM(--MID(A2, ROW(INDIRECT("1:"&LEN(A2))), 1)*10^(LEN(A2)-ROW(INDIRECT("1:"&LEN(A2)))))}
4. 中英文混合处理:
# 注意:中文字符也占1个字符位
="你好世界ABC"
=LEFT(A1, 2) # 你好
=RIGHT(A1, 3) # ABC
# LENB统计字节数(中文占2字节)
=LENB("你好世界") # 8
=LEN("你好世界") # 4
常见错误处理:
# 找不到分隔符时返回错误
=LEFT(A2, FIND("@", A2)-1) # 如果没有@会报错
# 使用IFERROR处理
=IFERROR(LEFT(A2, FIND("@", A2)-1), A2)
# 找不到@时返回原文本
# 或使用IFNA(仅处理#N/A错误)
=IFNA(LEFT(A2, FIND("@", A2)-1), A2)
性能优化技巧:
# 避免多次计算相同的FIND
# 差
=MID(A2, FIND("@", A2)+1, FIND(".", A2, FIND("@", A2))-FIND("@", A2)-1)
# 好:使用辅助列存储FIND结果
# B2: =FIND("@", A2)
# C2: =FIND(".", A2, B2)
# D2: =MID(A2, B2+1, C2-B2-1)
# 或使用LET函数(Excel 365)
=LET(
at_pos, FIND("@", A2),
dot_pos, FIND(".", A2, at_pos),
MID(A2, at_pos+1, dot_pos-at_pos-1)
)
▶ 如何使用数据验证(下拉列表)?
数据验证用于限制单元格输入内容,确保数据准确性和规范性。
创建基础下拉列表:
- 选中目标单元格
- 数据 → 数据验证 → 数据验证
- 允许:列表
- 来源:输入选项(逗号分隔)或引用区域
方法1:直接输入选项:
验证条件:列表
来源:男,女,其他
优点:简单快捷 缺点:选项固定,不易维护
方法2:引用单元格区域:
来源:=$G$2:$G$10
说明:
- 在G2:G10输入选项列表
- 使用绝对引用($)
- 可隐藏G列或放在其他工作表
优点:选项可维护,修改G列即可 缺点:需要占用单元格
方法3:使用定义名称:
步骤:
- 选中G2:G10
- 公式 → 定义名称 → 输入"部门列表"
- 数据验证来源:=部门列表
优点:公式更清晰,易于管理 缺点:需要额外定义名称
实战案例:
场景1:二级联动下拉列表
需求:选择省份后,城市列表自动更新
步骤:
- 准备数据:
H列(省份) I列(城市)
北京 北京市
北京 通州区
北京 朝阳区
上海 上海市
上海 浦东新区
上海 黄浦区
- 创建省份下拉(A2单元格):
数据验证来源:北京,上海,广州
# 或引用唯一省份列表
- 创建动态城市列表(B2单元格):
数据验证来源:=INDIRECT(A2)
但需要先为每个省份创建命名范围:
- 选中"北京市,通州区,朝阳区",定义名称为"北京"
- 选中"上海市,浦东新区,黄浦区",定义名称为"上海"
更灵活的方法(使用FILTER,Excel 365):
数据验证来源:=FILTER($I$2:$I$20, $H$2:$H$20=A2)
场景2:动态扩展的下拉列表
需求:选项列表会不断增加
方法1:使用动态命名区域
名称管理器定义:
名称:动态列表
引用位置:=OFFSET(Sheet1!$G$2,0,0,COUNTA(Sheet1!$G:$G)-1,1)
数据验证来源:=动态列表
方法2:使用表格(推荐)
- 选中G1:G20
- 插入 → 表 → 勾选"表包含标题"
- 数据验证来源:
=Table1[部门]
优点:自动扩展,无需公式维护
场景3:条件限制(不同行不同选项)
需求:
- A列"类型"选择"收入"时,B列只能选收入类别
- A列选择"支出"时,B列只能选支出类别
设置B2单元格验证:
数据验证来源:=IF($A2="收入", 收入列表, 支出列表)
其中"收入列表"和"支出列表"是预定义的命名区域。
或使用CHOOSE:
=CHOOSE(MATCH($A2,{"收入","支出"},0),
$G$2:$G$5, # 收入选项
$H$2:$H$8) # 支出选项
场景4:允许自定义输入
需求:提供常用选项,但允许输入其他值
方法:不使用数据验证,用VBA或公式提示
或使用"提示信息"功能:
- 数据验证 → 输入信息
- 标题:常用选项
- 输入信息:财务部,人力部,技术部(或输入其他)
注意:这只是提示,不强制限制
真正允许自定义的变通方法:
- 不设验证,使用条件格式标记非标准值
- 条件格式公式:
=NOT(COUNTIF(标准列表, A2)) - 格式:黄色背景(提醒但不阻止)
其他验证类型:
1. 数字范围:
允许:整数
数据:介于
最小值:1
最大值:100
或使用公式:
允许:自定义
公式:=AND(A2>=1, A2<=100, MOD(A2,1)=0)
2. 文本长度:
允许:文本长度
数据:等于
长度:18 # 身份证号必须18位
或:
允许:自定义
公式:=LEN(A2)=18
3. 日期范围:
允许:日期
数据:介于
开始日期:=TODAY()
结束日期:=TODAY()+30 # 只能选未来30天
4. 唯一值(不重复):
允许:自定义
公式:=COUNTIF($A$2:$A$100, A2)<=1
5. 手机号格式:
允许:自定义
公式:=AND(LEN(A2)=11, LEFT(A2,1)="1", ISNUMBER(--A2))
6. 邮箱格式:
允许:自定义
公式:=AND(ISERROR(FIND(" ", A2)), LEN(A2)-LEN(SUBSTITUTE(A2,"@",""))=1, FIND("@", A2)>1, FIND(".", A2, FIND("@", A2))>FIND("@", A2)+1)
提示信息设置:
输入信息标签:
标题:请选择部门
输入信息:从列表中选择您所属的部门,或留空
出错警告标签:
样式:停止/警告/信息
标题:输入错误
错误信息:请从下拉列表中选择,或输入1-100之间的整数
样式区别:
- 停止:完全阻止无效输入(图标:❌)
- 警告:提示但可选择继续(图标:⚠)
- 信息:仅提示不阻止(图标:ℹ)
批量应用验证:
- 设置第一个单元格的验证
- 复制该单元格(Ctrl+C)
- 选中目标区域
- 选择性粘贴 → 验证
或:
- 选中目标区域(包括已设验证的)
- 数据 → 数据验证
- 勾选"将设置应用于其他相同设置的单元格"
清除验证:
- 选中单元格
- 数据 → 数据验证
- 全部清除
查找包含验证的单元格:
- 开始 → 查找和选择 → 定位条件
- 选择"数据验证" → 全部/相同
- 确定
常见问题:
- 下拉箭头不显示:检查是否勾选"提供下拉箭头"
- INDIRECT不工作:确保命名区域名称没有空格或特殊字符
- 验证失效:复制粘贴可能覆盖验证,使用"选择性粘贴-数值"
- 动态列表不更新:使用表格或OFFSET公式确保范围动态扩展
▶ 如何使用日期时间函数进行计算?
Excel的日期时间函数是数据分析中的核心技能,掌握它们能高效处理时间序列数据。
基础日期时间函数:
-- 当前日期时间
=TODAY() # 2024-01-15(仅日期)
=NOW() # 2024-01-15 14:30:25(日期+时间)
-- 构造日期时间
=DATE(2024, 1, 15) # 2024-01-15
=TIME(14, 30, 0) # 14:30:00(0.604166,实际是小数)
=DATEVALUE("2024-01-15") # 将文本转日期
=TIMEVALUE("14:30:00") # 将文本转时间
-- 提取日期部分
=YEAR(A1) # 提取年份:2024
=MONTH(A1) # 提取月份:1
=DAY(A1) # 提取日期:15
=HOUR(A1) # 提取小时:14
=MINUTE(A1) # 提取分钟:30
=SECOND(A1) # 提取秒数:25
-- 星期处理
=WEEKDAY(A1) # 1-7(1=周日,7=周六)
=WEEKDAY(A1, 2) # 1-7(1=周一,7=周日)
=TEXT(A1, "aaaa") # 星期一/Monday
=TEXT(A1, "aaa") # 周一/Mon
=WEEKNUM(A1) # 返回第几周
日期格式化(TEXT函数):
日期格式代码:
=TEXT(A1, "yyyy-mm-dd") # 2024-01-15
=TEXT(A1, "yyyy年mm月dd日") # 2024年01月15日
=TEXT(A1, "m/d/yyyy") # 1/15/2024
=TEXT(A1, "mmm dd, yyyy") # Jan 15, 2024
=TEXT(A1, "mmmm dd, yyyy") # January 15, 2024
=TEXT(A1, "dddd, mmmm dd, yyyy") # Monday, January 15, 2024
# 格式符号说明:
# yyyy: 4位年份 yy: 2位年份
# mm: 2位月份 m: 1位月份
# mmm: 月份缩写 mmmm: 月份全称
# dd: 2位日期 d: 1位日期
# dddd: 星期全称 ddd: 星期缩写
时间格式代码:
=TEXT(A1, "hh:mm:ss") # 14:30:25(24小时制)
=TEXT(A1, "hh:mm AM/PM") # 02:30 PM(12小时制)
=TEXT(A1, "h:mm") # 14:30(不补零)
=TEXT(A1, "[h]:mm") # 总小时数(超过24小时显示)
# 格式符号说明:
# hh: 2位小时(24小时制) h: 1位小时
# mm: 2位分钟 m: 1位分钟
# ss: 2位秒 s: 1位秒
# [h]: 累计小时数(不归零)
组合与自定义:
=TEXT(A1, "yyyy-mm-dd hh:mm:ss") # 2024-01-15 14:30:25
=TEXT(A1, "yyyy/mm/dd (aaaa)") # 2024/01/15 (星期一)
=TEXT(A1, "mm月dd日 hh时mm分") # 01月15日 14时30分
# 条件格式化(不同值显示不同格式)
=TEXT(A1, "[>0]正数;[<0]负数;零")
=TEXT(A1, "[蓝色]yyyy-mm-dd") # 带颜色
中文日期处理:
=TEXT(A1, "yyyy年m月d日") # 2024年1月15日
=TEXT(A1, "aaaa") # 星期一
=TEXT(A1, "[$-zh-CN]aaaa") # 星期一(强制中文)
=TEXT(A1, "yyyy年第w周") # 2024年第3周
# 农历(需要特定函数或插件)
# Excel原生不支持农历,需借助第三方加载项
日期计算与运算:
日期加减运算:
-- 基本加减(日期是数字,1 = 1天)
=A1 + 7 # 加7天
=A1 - 30 # 减30天
=A1 + 1/24 # 加1小时(1天÷24)
=A1 + 30/1440 # 加30分钟(1天÷1440)
-- 使用 DATE 函数精确加减
=DATE(YEAR(A1), MONTH(A1) + 1, DAY(A1)) # 加1个月
=DATE(YEAR(A1) + 1, MONTH(A1), DAY(A1)) # 加1年
=DATE(YEAR(A1), MONTH(A1), DAY(A1) + 7) # 加7天
-- 处理月末溢出
=EDATE(A1, 1) # 加1个月(自动处理月末)
=EDATE(A1, -12) # 减12个月(即去年同期)
=EOMONTH(A1, 0) # 当月最后一天
=EOMONTH(A1, 1) # 下月最后一天
日期差值计算:
-- 简单相减(返回天数)
=B1 - A1 # 日期差(天数)
-- DAYS 函数
=DAYS(B1, A1) # 结束日期 - 开始日期
-- DATEDIF 函数(隐藏函数,强大但文档少)
=DATEDIF(A1, B1, "d") # 天数差
=DATEDIF(A1, B1, "m") # 完整月数差
=DATEDIF(A1, B1, "y") # 完整年数差
=DATEDIF(A1, B1, "ym") # 月数差(忽略年)
=DATEDIF(A1, B1, "md") # 天数差(忽略月和年)
=DATEDIF(A1, B1, "yd") # 天数差(忽略年)
-- 计算年龄(精确)
=DATEDIF(生日, TODAY(), "y") # 完整年龄
=DATEDIF(生日, TODAY(), "y") & "岁" &
DATEDIF(生日, TODAY(), "ym") & "个月" # 25岁3个月
-- 时间差计算(小时/分钟)
=(B1 - A1) * 24 # 小时差
=(B1 - A1) * 1440 # 分钟差
=(B1 - A1) * 86400 # 秒数差
工作日计算(排除周末和节假日):
-- WORKDAY:加N个工作日
=WORKDAY(A1, 5) # 5个工作日后(排除周六日)
=WORKDAY(A1, 10, H2:H10) # 10个工作日后(排除指定节假日)
=WORKDAY(A1, -5) # 5个工作日前
-- WORKDAY.INTL:自定义周末
=WORKDAY.INTL(A1, 10, 1) # 周六日为周末(默认)
=WORKDAY.INTL(A1, 10, 2) # 周日/周一为周末
=WORKDAY.INTL(A1, 10, 7) # 仅周日为周末
=WORKDAY.INTL(A1, 10, "0000011") # 自定义:周六日为周末
-- NETWORKDAYS:计算两日期间工作日数
=NETWORKDAYS(A1, B1) # 工作日天数(排除周六日)
=NETWORKDAYS(A1, B1, H2:H10) # 工作日天数(排除节假日)
=NETWORKDAYS.INTL(A1, B1, 1) # 自定义周末的工作日计算
-- 实战:计算项目工期
=NETWORKDAYS(项目开始日期, 项目结束日期, 节假日表)
月末月初与季度处理:
-- 月初
=DATE(YEAR(A1), MONTH(A1), 1) # 当月第一天
=EOMONTH(A1, -1) + 1 # 当月第一天(另一种方法)
-- 月末
=EOMONTH(A1, 0) # 当月最后一天
=DATE(YEAR(A1), MONTH(A1) + 1, 0) # 当月最后一天(另一种方法)
=EOMONTH(A1, 1) # 下月最后一天
=EOMONTH(A1, -1) # 上月最后一天
-- 判断是否月末
=A1 = EOMONTH(A1, 0) # TRUE/FALSE
-- 当月天数
=DAY(EOMONTH(A1, 0)) # 28/29/30/31
-- 季度处理
=ROUNDUP(MONTH(A1) / 3, 0) # 返回季度(1-4)
="Q" & ROUNDUP(MONTH(A1) / 3, 0) # Q1/Q2/Q3/Q4
=QUARTER(A1) # 季度(Excel 365)
-- 季度初/季度末
=DATE(YEAR(A1), (ROUNDUP(MONTH(A1)/3, 0) - 1) * 3 + 1, 1) # 季度初
=EOMONTH(A1, (ROUNDUP(MONTH(A1)/3, 0) * 3) - MONTH(A1)) # 季度末
实战场景应用:
场景1:计算员工入职天数和工龄:
# A2: 入职日期
=DATEDIF(A2, TODAY(), "d") & "天" # 入职总天数
=DATEDIF(A2, TODAY(), "y") & "年" &
DATEDIF(A2, TODAY(), "ym") & "月" # 1年3个月
=NETWORKDAYS(A2, TODAY()) # 工作日数
场景2:生成日期序列(用于月报/周报):
# 生成连续日期(从A1开始往下拖拽公式)
=A1 + 1 # 每天+1
=DATE(YEAR(A1), MONTH(A1) + 1, 1) # 每月1日
=A1 + 7 # 每周
# SEQUENCE函数(Excel 365)
=SEQUENCE(30, 1, TODAY(), 1) # 从今天开始30天序列
=SEQUENCE(12, 1, DATE(2024,1,1), 30) # 每月1日(12个月)
场景3:计算账期和逾期天数:
# A2: 订单日期 B2: 账期天数(如30天) C2: 还款日期
=A2 + B2 # 应还款日期
=IF(C2 > A2 + B2, C2 - (A2 + B2), 0) # 逾期天数
=IF(TODAY() > A2 + B2, TODAY() - (A2 + B2), 0) # 当前逾期天数
场景4:按月/季度汇总数据(数据透视表配合):
# 提取年月(用于分组)
=TEXT(A2, "yyyy-mm") # 2024-01
=YEAR(A2) & "-Q" & ROUNDUP(MONTH(A2)/3, 0) # 2024-Q1
# SUMIFS按月统计
=SUMIFS(金额列, 日期列, ">=" & DATE(2024,1,1), 日期列, "<" & DATE(2024,2,1))
场景5:计算同比/环比:
# 去年同期(同比)
=DATE(YEAR(A2) - 1, MONTH(A2), DAY(A2)) # 去年同一天
=EDATE(A2, -12) # 去年同月
# 上个月(环比)
=DATE(YEAR(A2), MONTH(A2) - 1, DAY(A2))
=EDATE(A2, -1)
# 同比增长率
=(本期金额 - VLOOKUP(去年同期日期, 历史表, 金额列, 0)) /
VLOOKUP(去年同期日期, 历史表, 金额列, 0)
场景6:判断节假日和工作日:
# 判断是否周末
=WEEKDAY(A2, 2) > 5 # TRUE=周末
=IF(WEEKDAY(A2,2)>5, "周末", "工作日")
# 判断是否法定节假日
=COUNTIF(节假日表, A2) > 0 # TRUE=节假日
# 综合判断
=IF(OR(WEEKDAY(A2,2)>5, COUNTIF(节假日表,A2)>0), "休息日", "工作日")
场景7:提取动态时间段数据:
# 本月数据
=AND(A2>=DATE(YEAR(TODAY()), MONTH(TODAY()), 1),
A2<=EOMONTH(TODAY(), 0))
# 最近7天
=AND(A2>=TODAY()-7, A2<=TODAY())
# 本季度
=AND(ROUNDUP(MONTH(A2)/3,0) = ROUNDUP(MONTH(TODAY())/3,0),
YEAR(A2) = YEAR(TODAY()))
常见错误与解决:
| 问题 | 原因 | 解决方案 |
|---|---|---|
#VALUE! | 日期格式错误(文本) | 用 DATEVALUE() 转换或检查单元格格式 |
#NUM! | 日期超出范围(1900-9999) | 检查输入数据 |
| 日期显示为数字(45326) | 单元格格式非日期 | 右键 → 设置单元格格式 → 日期 |
| DATEDIF返回负数 | 开始日期晚于结束日期 | 交换参数或加绝对值 ABS() |
| 月末日期不对 | 跨月计算溢出 | 使用 EDATE() 或 EOMONTH() 代替加减 |
Excel日期系统注意事项:
- Excel日期本质是数字:1900-01-01 = 1,每天+1
- 时间是小数:1小时 = 1/24 = 0.041667
- 1900闰年Bug:Excel错误地将1900年当闰年(兼容Lotus 1-2-3)
- 不同系统:Windows(1900系统)vs Mac(1904系统)可能导致日期差异
- 文本vs日期:从CSV导入的"2024-01-15"可能是文本,需转换
- 格式保留:复制粘贴时使用"选择性粘贴-值"避免格式问题
Pandas数据处理基础
▶ 如何读取和查看数据的基本信息?
数据读取和初步检查是数据分析的第一步,需要快速了解数据概况。
场景:刚拿到一个CSV文件,需要快速了解数据结构和质量。
import pandas as pd
import numpy as np
# 读取数据(常见格式)
df_csv = pd.read_csv('data.csv', encoding='utf-8')
df_excel = pd.read_excel('data.xlsx', sheet_name='Sheet1')
df_json = pd.read_json('data.json')
# 数据库读取
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://user:pass@localhost/db')
df_sql = pd.read_sql('SELECT * FROM table', engine)
# 快速查看数据
print(df.head()) # 前5行
print(df.tail(10)) # 后10行
print(df.sample(5)) # 随机5行
# 数据概览
print(df.info()) # 列类型、非空值数量、内存占用
print(df.describe()) # 数值列的统计摘要
print(df.shape) # (行数, 列数)
print(df.columns) # 列名列表
print(df.dtypes) # 每列的数据类型
# 检查数据质量
print(df.isnull().sum()) # 每列的缺失值数量
print(df.duplicated().sum()) # 重复行数量
print(df.nunique()) # 每列的唯一值数量
# 内存优化检查
print(df.memory_usage(deep=True)) # 每列的内存占用
print(f"Total: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
数据质量报告函数:
def data_quality_report(df):
"""
生成数据质量报告
"""
report = pd.DataFrame({
'列名': df.columns,
'数据类型': df.dtypes.values,
'非空值': df.notnull().sum().values,
'空值数量': df.isnull().sum().values,
'空值占比': (df.isnull().sum() / len(df) * 100).round(2).values,
'唯一值数': df.nunique().values,
'重复值': [df[col].duplicated().sum() for col in df.columns]
})
# 添加示例值
report['示例值'] = [
str(df[col].dropna().iloc[0]) if len(df[col].dropna()) > 0 else 'N/A'
for col in df.columns
]
return report
# 使用示例
report = data_quality_report(df)
print(report)
读取大文件技巧:
# 分块读取
chunksize = 10000
chunks = []
for chunk in pd.read_csv('large_file.csv', chunksize=chunksize):
# 对每个块进行处理
chunk_processed = chunk[chunk['age'] > 18]
chunks.append(chunk_processed)
df = pd.concat(chunks, ignore_index=True)
# 只读取需要的列
df = pd.read_csv('data.csv', usecols=['col1', 'col2', 'col3'])
# 指定数据类型减少内存
df = pd.read_csv('data.csv', dtype={
'user_id': 'int32',
'age': 'int8',
'category': 'category'
})
# 边读边过滤
df = pd.read_csv('data.csv',
nrows=10000, # 只读前10000行
skiprows=range(1, 100)) # 跳过第1-99行
▶ 如何进行数据选择和筛选?
数据选择是Pandas最常用的操作,需要掌握多种筛选方式。
基础选择:
import pandas as pd
# 创建示例数据
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'age': [25, 30, 35, 28, 32],
'city': ['Beijing', 'Shanghai', 'Beijing', 'Guangzhou', 'Shanghai'],
'salary': [8000, 12000, 15000, 9000, 11000]
})
# 选择列
df['name'] # 单列,返回Series
df[['name', 'age']] # 多列,返回DataFrame
# 选择行(按位置)
df.iloc[0] # 第一行
df.iloc[0:3] # 前3行(0,1,2)
df.iloc[[0, 2, 4]] # 特定行
# 选择行(按标签)
df.loc[0] # 索引为0的行
df.loc[0:2] # 索引0到2(包含2)
df.loc[df['age'] > 30] # 条件筛选
# 选择行列
df.loc[0:2, ['name', 'age']] # 行0-2的name和age列
df.iloc[0:3, 0:2] # 前3行的前2列
# 快速访问单个值
df.at[0, 'name'] # 标签访问
df.iat[0, 0] # 位置访问
条件筛选:
# 单条件
df[df['age'] > 30]
df[df['city'] == 'Beijing']
df[df['salary'] >= 10000]
# 多条件(与)
df[(df['age'] > 25) & (df['salary'] >= 10000)]
# 多条件(或)
df[(df['city'] == 'Beijing') | (df['city'] == 'Shanghai')]
# 取反
df[~(df['age'] > 30)]
# 范围筛选
df[df['age'].between(25, 30)]
df[df['age'].between(25, 30, inclusive='neither')] # 不包含边界
# IN条件
cities = ['Beijing', 'Shanghai']
df[df['city'].isin(cities)]
# NOT IN
df[~df['city'].isin(cities)]
# 字符串匹配
df[df['name'].str.contains('li')] # 包含'li'
df[df['name'].str.startswith('A')] # 以A开头
df[df['name'].str.endswith('e')] # 以e结尾
# 正则表达式
df[df['name'].str.match(r'^[A-C]')] # 以A-C开头
# 空值筛选
df[df['age'].isnull()] # 空值
df[df['age'].notnull()] # 非空值
高级筛选:
# query方法(更简洁)
df.query('age > 30')
df.query('age > 30 and salary >= 10000')
df.query('city in ["Beijing", "Shanghai"]')
df.query('age > @min_age', local_dict={'min_age': 25})
# 使用变量
threshold = 30
df.query('age > @threshold')
# filter方法(按列名筛选)
df.filter(like='age') # 列名包含'age'
df.filter(regex='^na') # 列名以na开头
df.filter(items=['name', 'age']) # 指定列名
# where方法(条件替换)
df.where(df['age'] > 30, 0) # 不满足条件的替换为0
# mask方法(与where相反)
df.mask(df['age'] > 30, 0) # 满足条件的替换为0
实战案例:用户筛选:
# 找出高薪且年轻的一线城市用户
result = df[
(df['age'] < 30) &
(df['salary'] >= 10000) &
(df['city'].isin(['Beijing', 'Shanghai', 'Guangzhou', 'Shenzhen']))
]
# 找出名字包含特定字符且薪资在某范围的用户
result = df[
df['name'].str.contains('a', case=False) &
df['salary'].between(8000, 12000)
]
# 排除异常值(3倍标准差)
mean = df['salary'].mean()
std = df['salary'].std()
df_cleaned = df[
(df['salary'] >= mean - 3*std) &
(df['salary'] <= mean + 3*std)
]
▶ 如何处理缺失值?
缺失值处理是数据清洗的核心任务,需要根据业务场景选择合适的策略。
识别缺失值:
import pandas as pd
import numpy as np
# 创建包含缺失值的数据
df = pd.DataFrame({
'A': [1, 2, np.nan, 4, 5],
'B': [np.nan, 2, 3, np.nan, 5],
'C': [1, 2, 3, 4, 5],
'D': ['a', None, 'c', 'd', 'e']
})
# 检查缺失值
df.isnull() # 布尔DataFrame
df.notnull() # 非空布尔
df.isnull().sum() # 每列缺失值数量
df.isnull().sum().sum() # 总缺失值数量
df.isnull().any() # 每列是否有缺失值
df.isnull().all() # 每列是否全是缺失值
# 缺失值可视化统计
missing_stats = pd.DataFrame({
'缺失数量': df.isnull().sum(),
'缺失比例': (df.isnull().sum() / len(df) * 100).round(2)
})
print(missing_stats)
删除缺失值:
# 删除包含缺失值的行
df.dropna() # 任意列有缺失就删除
df.dropna(how='all') # 全部列都是缺失才删除
df.dropna(subset=['A', 'B']) # 只看A和B列
df.dropna(thresh=3) # 至少3个非空值才保留
# 删除包含缺失值的列
df.dropna(axis=1) # 任意行有缺失就删除该列
df.dropna(axis=1, how='all') # 全部行都是缺失才删除
# 原地修改
df.dropna(inplace=True)
填充缺失值:
# 固定值填充
df.fillna(0) # 全部填0
df.fillna({'A': 0, 'B': -1}) # 不同列填不同值
# 统计值填充
df.fillna(df.mean()) # 均值填充(仅数值列)
df.fillna(df.median()) # 中位数填充
df.fillna(df.mode().iloc[0]) # 众数填充
# 前后值填充
df.fillna(method='ffill') # 前向填充
df.fillna(method='bfill') # 后向填充
df.fillna(method='ffill', limit=2) # 最多填充2个连续缺失
# 插值填充
df.interpolate() # 线性插值
df.interpolate(method='polynomial', order=2) # 多项式插值
df.interpolate(method='time') # 时间序列插值
高级填充策略:
# 分组填充(按类别用不同均值填充)
df['salary'] = df.groupby('department')['salary'].transform(
lambda x: x.fillna(x.mean())
)
# 条件填充
df['age'] = df['age'].fillna(
df['age'].median() if df['age'].median() > 0 else 18
)
# 用其他列的值填充
df['email'] = df['email'].fillna(df['phone'])
# 复杂逻辑填充
def smart_fill(row):
if pd.isnull(row['value']):
if row['type'] == 'A':
return row['value_A']
elif row['type'] == 'B':
return row['value_B']
else:
return 0
return row['value']
df['value'] = df.apply(smart_fill, axis=1)
实战案例:用户数据清洗:
# 场景:用户信息表包含age、income、city等字段
user_df = pd.DataFrame({
'user_id': range(1, 11),
'age': [25, np.nan, 30, np.nan, 35, 28, np.nan, 32, 29, 31],
'income': [8000, 10000, np.nan, 12000, 15000, np.nan, 11000, np.nan, 9000, 13000],
'city': ['Beijing', None, 'Shanghai', 'Beijing', None, 'Shanghai', 'Guangzhou', 'Beijing', None, 'Shanghai']
})
# 清洗策略
# 1. age缺失:用中位数填充
user_df['age'] = user_df['age'].fillna(user_df['age'].median())
# 2. income缺失:按城市分组填充均值
user_df['income'] = user_df.groupby('city')['income'].transform(
lambda x: x.fillna(x.mean())
)
# 3. city缺失:用众数填充
user_df['city'] = user_df['city'].fillna(user_df['city'].mode()[0])
# 4. 如果某行缺失过多(>50%),删除
threshold = len(user_df.columns) * 0.5
user_df_cleaned = user_df.dropna(thresh=threshold)
print("清洗前缺失值:")
print(user_df.isnull().sum())
print("\n清洗后缺失值:")
print(user_df_cleaned.isnull().sum())
检测填充合理性:
# 填充前后对比
print("原始统计:")
print(df['salary'].describe())
df['salary_filled'] = df['salary'].fillna(df['salary'].mean())
print("\n填充后统计:")
print(df['salary_filled'].describe())
# 可视化对比(需要matplotlib)
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
df['salary'].hist(ax=axes[0], bins=20)
axes[0].set_title('填充前分布')
df['salary_filled'].hist(ax=axes[1], bins=20)
axes[1].set_title('填充后分布')
plt.tight_layout()
plt.show()
▶ 如何进行数据分组聚合?
分组聚合是数据分析的核心操作,用于计算各类别的统计指标。
基础分组:
import pandas as pd
# 示例数据
df = pd.DataFrame({
'department': ['Sales', 'Sales', 'IT', 'IT', 'HR', 'HR'],
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve', 'Frank'],
'salary': [8000, 9000, 12000, 13000, 7000, 7500],
'age': [25, 30, 35, 28, 32, 29],
'performance': [85, 90, 88, 92, 78, 82]
})
# 单列分组
grouped = df.groupby('department')
# 多列分组
grouped_multi = df.groupby(['department', 'age'])
# 查看分组
for name, group in grouped:
print(f"\n{name}:")
print(group)
# 获取单个组
sales_group = grouped.get_group('Sales')
聚合函数:
# 单个聚合
df.groupby('department')['salary'].mean()
df.groupby('department')['salary'].sum()
df.groupby('department')['salary'].max()
df.groupby('department')['salary'].min()
df.groupby('department')['salary'].count()
df.groupby('department')['salary'].std()
# 多个聚合
df.groupby('department')['salary'].agg(['mean', 'sum', 'count'])
# 不同列用不同聚合
df.groupby('department').agg({
'salary': ['mean', 'sum'],
'age': ['min', 'max'],
'performance': 'mean'
})
# 自定义聚合函数
def salary_range(x):
return x.max() - x.min()
df.groupby('department')['salary'].agg([
'mean',
('salary_range', salary_range),
('top_salary', 'max')
])
# lambda函数
df.groupby('department')['salary'].agg([
('avg', 'mean'),
('range', lambda x: x.max() - x.min()),
('top3_avg', lambda x: x.nlargest(3).mean())
])
transform方法:
# 将分组结果广播回原DataFrame
df['dept_avg_salary'] = df.groupby('department')['salary'].transform('mean')
df['salary_rank_in_dept'] = df.groupby('department')['salary'].rank(ascending=False)
df['dept_count'] = df.groupby('department')['salary'].transform('count')
# 与组内均值的偏差
df['salary_deviation'] = df['salary'] - df.groupby('department')['salary'].transform('mean')
print(df)
filter方法:
# 过滤组
# 保留平均工资>8000的部门
df.groupby('department').filter(lambda x: x['salary'].mean() > 8000)
# 保留人数>=2的部门
df.groupby('department').filter(lambda x: len(x) >= 2)
# 保留最高工资>10000的部门
df.groupby('department').filter(lambda x: x['salary'].max() > 10000)
实战案例:销售分析:
# 销售数据
sales_df = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=100),
'region': np.random.choice(['North', 'South', 'East', 'West'], 100),
'product': np.random.choice(['A', 'B', 'C'], 100),
'sales_amount': np.random.randint(1000, 10000, 100),
'quantity': np.random.randint(1, 20, 100)
})
# 1. 按地区统计总销售额和平均单价
region_summary = sales_df.groupby('region').agg({
'sales_amount': ['sum', 'mean', 'count'],
'quantity': 'sum'
})
region_summary.columns = ['_'.join(col) for col in region_summary.columns]
region_summary['avg_price'] = region_summary['sales_amount_sum'] / region_summary['quantity_sum']
print(region_summary)
# 2. 按地区和产品交叉统计
pivot_summary = sales_df.pivot_table(
values='sales_amount',
index='region',
columns='product',
aggfunc=['sum', 'mean'],
fill_value=0,
margins=True # 添加总计行列
)
print(pivot_summary)
# 3. 时间序列分组(按月)
sales_df['month'] = sales_df['date'].dt.to_period('M')
monthly_sales = sales_df.groupby('month').agg({
'sales_amount': 'sum',
'quantity': 'sum'
})
monthly_sales['unit_price'] = monthly_sales['sales_amount'] / monthly_sales['quantity']
print(monthly_sales)
# 4. 多维分组:地区+产品+月份
multi_dim = sales_df.groupby([
sales_df['date'].dt.to_period('M'),
'region',
'product'
])['sales_amount'].sum().unstack(fill_value=0)
print(multi_dim)
# 5. 动态排名:每个地区销售额Top3的日期
top_days = sales_df.groupby('region').apply(
lambda x: x.nlargest(3, 'sales_amount')[['date', 'sales_amount']]
).reset_index(drop=True)
print(top_days)
# 6. 累计计算
sales_df_sorted = sales_df.sort_values(['region', 'date'])
sales_df_sorted['cumulative_sales'] = sales_df_sorted.groupby('region')['sales_amount'].cumsum()
sales_df_sorted['rolling_avg_7d'] = sales_df_sorted.groupby('region')['sales_amount'].transform(
lambda x: x.rolling(window=7, min_periods=1).mean()
)
性能优化技巧:
# 1. 使用observed=True(分类数据)
df['department'] = df['department'].astype('category')
df.groupby('department', observed=True)['salary'].mean()
# 2. 使用agg替代多次聚合
# 慢
mean_salary = df.groupby('department')['salary'].mean()
sum_salary = df.groupby('department')['salary'].sum()
# 快
result = df.groupby('department')['salary'].agg(['mean', 'sum'])
# 3. 使用numba加速自定义聚合(大数据)
from numba import jit
@jit
def custom_agg(arr):
return arr.max() - arr.min()
# 可以结合groupby使用
Pandas数据合并与重塑
▶ 如何合并多个DataFrame?
数据合并是将来自不同源的数据整合在一起,类似SQL的JOIN操作。
三种合并方法对比:
| 特性 | merge | concat | join |
|---|---|---|---|
| 主要用途 | 基于列值连接(SQL JOIN) | 拼接(堆叠) | 基于索引连接 |
| 连接依据 | 指定列(on) | 行/列索引 | DataFrame索引 |
| 方向 | 根据键值匹配 | 纵向(axis=0)或横向(axis=1) | 横向(列合并) |
| 连接类型 | inner/left/right/outer | 默认outer | left/right/outer/inner |
| 性能 | 中等 | 快(简单拼接) | 快(索引优化) |
| 适用场景 | 多表关联(如订单-用户) | 批量数据追加 | 索引对齐合并 |
| SQL对应 | JOIN | UNION ALL | JOIN ON index |
| 典型用法 | pd.merge(df1, df2, on='key') | pd.concat([df1, df2]) | df1.join(df2) |
merge合并(类似SQL JOIN):
import pandas as pd
# 示例数据
users = pd.DataFrame({
'user_id': [1, 2, 3, 4],
'name': ['Alice', 'Bob', 'Charlie', 'David']
})
orders = pd.DataFrame({
'order_id': [101, 102, 103, 104, 105],
'user_id': [1, 1, 2, 3, 5],
'amount': [100, 150, 200, 120, 180]
})
# 内连接(INNER JOIN)
inner_merged = pd.merge(users, orders, on='user_id', how='inner')
print("内连接:")
print(inner_merged)
# 左连接(LEFT JOIN)
left_merged = pd.merge(users, orders, on='user_id', how='left')
print("\n左连接:")
print(left_merged)
# 右连接(RIGHT JOIN)
right_merged = pd.merge(users, orders, on='user_id', how='right')
print("\n右连接:")
print(right_merged)
# 外连接(FULL OUTER JOIN)
outer_merged = pd.merge(users, orders, on='user_id', how='outer')
print("\n外连接:")
print(outer_merged)
# 不同列名连接
users2 = users.rename(columns={'user_id': 'uid'})
merged = pd.merge(users2, orders, left_on='uid', right_on='user_id')
# 多列连接
df1 = pd.DataFrame({
'key1': ['A', 'A', 'B', 'B'],
'key2': [1, 2, 1, 2],
'value1': [10, 20, 30, 40]
})
df2 = pd.DataFrame({
'key1': ['A', 'A', 'B', 'C'],
'key2': [1, 2, 2, 1],
'value2': [100, 200, 300, 400]
})
merged_multi = pd.merge(df1, df2, on=['key1', 'key2'], how='outer')
concat拼接:
# 纵向拼接(行拼接)
df1 = pd.DataFrame({'A': [1, 2], 'B': [3, 4]})
df2 = pd.DataFrame({'A': [5, 6], 'B': [7, 8]})
# 直接拼接
result = pd.concat([df1, df2])
print("纵向拼接:")
print(result)
# 重置索引
result = pd.concat([df1, df2], ignore_index=True)
# 横向拼接(列拼接)
df3 = pd.DataFrame({'C': [9, 10], 'D': [11, 12]})
result = pd.concat([df1, df3], axis=1)
print("\n横向拼接:")
print(result)
# 带标签的拼接
result = pd.concat([df1, df2], keys=['batch1', 'batch2'])
print("\n多层索引拼接:")
print(result)
# 只保留共同列
df_diff_cols = pd.DataFrame({'A': [7, 8], 'C': [9, 10]})
result = pd.concat([df1, df_diff_cols], join='inner')
join方法(基于索引):
# 准备数据(设置索引)
df_left = pd.DataFrame({
'value1': [1, 2, 3]
}, index=['A', 'B', 'C'])
df_right = pd.DataFrame({
'value2': [4, 5, 6]
}, index=['A', 'B', 'D'])
# 基于索引join
result = df_left.join(df_right, how='outer')
print("基于索引join:")
print(result)
# 多DataFrame join
df1 = pd.DataFrame({'A': [1, 2]}, index=[0, 1])
df2 = pd.DataFrame({'B': [3, 4]}, index=[0, 1])
df3 = pd.DataFrame({'C': [5, 6]}, index=[0, 1])
result = df1.join([df2, df3])
实战案例:多表关联分析:
# 场景:分析用户购买行为
# 表1:用户信息
users = pd.DataFrame({
'user_id': [1, 2, 3, 4, 5],
'name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
'city': ['Beijing', 'Shanghai', 'Beijing', 'Guangzhou', 'Shanghai'],
'vip_level': [1, 2, 1, 3, 2]
})
# 表2:订单信息
orders = pd.DataFrame({
'order_id': [101, 102, 103, 104, 105, 106],
'user_id': [1, 1, 2, 3, 3, 5],
'order_date': pd.date_range('2024-01-01', periods=6),
'amount': [100, 150, 200, 120, 180, 90]
})
# 表3:产品信息
products = pd.DataFrame({
'order_id': [101, 102, 103, 104, 105, 106],
'product_id': ['P1', 'P2', 'P1', 'P3', 'P2', 'P1'],
'product_name': ['手机', '电脑', '手机', '平板', '电脑', '手机'],
'category': ['电子', '电子', '电子', '电子', '电子', '电子']
})
# Step 1: 关联订单和用户
merged_data = pd.merge(orders, users, on='user_id', how='left')
# Step 2: 关联产品信息
merged_data = pd.merge(merged_data, products, on='order_id', how='left')
print("完整关联数据:")
print(merged_data.head())
# Step 3: 分析
# 每个城市的销售额
city_sales = merged_data.groupby('city')['amount'].agg(['sum', 'mean', 'count'])
print("\n各城市销售统计:")
print(city_sales)
# VIP等级的购买力
vip_analysis = merged_data.groupby('vip_level').agg({
'amount': ['sum', 'mean'],
'order_id': 'count'
})
vip_analysis.columns = ['总消费', '平均订单金额', '订单数']
print("\nVIP分析:")
print(vip_analysis)
# 产品销售排行
product_rank = merged_data.groupby('product_name')['amount'].agg([
('销售额', 'sum'),
('订单数', 'count')
]).sort_values('销售额', ascending=False)
print("\n产品销售排行:")
print(product_rank)
# 用户消费明细(加入统计列)
user_summary = merged_data.groupby('user_id').agg({
'amount': ['sum', 'mean', 'count'],
'product_id': lambda x: x.nunique()
})
user_summary.columns = ['总消费', '平均订单', '订单数', '购买产品种类']
# 关联回用户表
final_report = pd.merge(users, user_summary, on='user_id', how='left')
final_report = final_report.fillna(0)
print("\n用户消费汇总:")
print(final_report)
合并性能优化:
# 1. 提前过滤数据
filtered_orders = orders[orders['amount'] > 100]
merged = pd.merge(users, filtered_orders, on='user_id')
# 2. 使用分类类型
users['city'] = users['city'].astype('category')
# 3. 设置索引加速(频繁merge)
users_indexed = users.set_index('user_id')
orders_indexed = orders.set_index('user_id')
merged = users_indexed.join(orders_indexed, how='left')
# 4. 批量merge
# 慢
result = df1
for df in [df2, df3, df4]:
result = pd.merge(result, df, on='key')
# 快
from functools import reduce
dfs = [df1, df2, df3, df4]
result = reduce(lambda left, right: pd.merge(left, right, on='key'), dfs)
▶ 如何进行数据透视和重塑?
数据透视可以将长格式数据转为宽格式,重塑可以将宽格式转为长格式。
pivot_table透视表:
import pandas as pd
import numpy as np
# 示例数据
sales = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=12),
'region': ['North', 'South'] * 6,
'product': ['A', 'B', 'A', 'B'] * 3,
'sales': np.random.randint(100, 500, 12),
'units': np.random.randint(10, 50, 12)
})
# 基础透视
pivot1 = sales.pivot_table(
values='sales',
index='region',
columns='product',
aggfunc='sum'
)
print("基础透视:")
print(pivot1)
# 多层索引
pivot2 = sales.pivot_table(
values='sales',
index=['region', sales['date'].dt.month],
columns='product',
aggfunc='sum'
)
print("\n多层索引透视:")
print(pivot2)
# 多个聚合值
pivot3 = sales.pivot_table(
values=['sales', 'units'],
index='region',
columns='product',
aggfunc='sum'
)
print("\n多值透视:")
print(pivot3)
# 多个聚合函数
pivot4 = sales.pivot_table(
values='sales',
index='region',
columns='product',
aggfunc=['sum', 'mean', 'count']
)
print("\n多聚合函数:")
print(pivot4)
# 添加边际合计
pivot5 = sales.pivot_table(
values='sales',
index='region',
columns='product',
aggfunc='sum',
margins=True, # 添加总计
margins_name='总计'
)
print("\n带总计的透视:")
print(pivot5)
# 填充缺失值
pivot6 = sales.pivot_table(
values='sales',
index='region',
columns='product',
aggfunc='sum',
fill_value=0
)
pivot简单透视:
# pivot不进行聚合,要求index+columns唯一
df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-01', '2024-01-02', '2024-01-02'],
'product': ['A', 'B', 'A', 'B'],
'sales': [100, 150, 120, 180]
})
pivot_simple = df.pivot(
index='date',
columns='product',
values='sales'
)
print("简单透视:")
print(pivot_simple)
melt逆透视(宽转长):
# 宽格式数据
wide_df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie'],
'Q1': [100, 120, 110],
'Q2': [110, 130, 115],
'Q3': [105, 125, 120],
'Q4': [115, 135, 125]
})
# 转为长格式
long_df = pd.melt(
wide_df,
id_vars=['name'], # 保持不变的列
value_vars=['Q1', 'Q2', 'Q3', 'Q4'], # 要转换的列
var_name='quarter', # 新列名(原列名)
value_name='sales' # 新列名(值)
)
print("宽转长:")
print(long_df)
# 所有列都转
long_df2 = pd.melt(wide_df, id_vars=['name'])
stack和unstack:
# unstack: 将行索引转为列索引
df = pd.DataFrame({
'region': ['North', 'North', 'South', 'South'],
'product': ['A', 'B', 'A', 'B'],
'sales': [100, 150, 120, 180]
})
df_indexed = df.set_index(['region', 'product'])
unstacked = df_indexed.unstack()
print("unstack效果:")
print(unstacked)
# stack: 将列索引转为行索引
stacked = unstacked.stack()
print("\nstack还原:")
print(stacked)
# 处理多层列索引
df_multi_col = pd.DataFrame({
('sales', 'A'): [100, 120],
('sales', 'B'): [150, 180],
('units', 'A'): [10, 12],
('units', 'B'): [15, 18]
}, index=['North', 'South'])
# 调整层级
swapped = df_multi_col.swaplevel(axis=1)
sorted_df = df_multi_col.sort_index(axis=1)
实战案例:销售报表生成:
# 原始数据:销售明细
np.random.seed(42)
sales_detail = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=90),
'region': np.random.choice(['North', 'South', 'East', 'West'], 90),
'product': np.random.choice(['A', 'B', 'C'], 90),
'channel': np.random.choice(['Online', 'Offline'], 90),
'sales': np.random.randint(1000, 10000, 90),
'cost': np.random.randint(500, 5000, 90)
})
# 添加月份
sales_detail['month'] = sales_detail['date'].dt.to_period('M')
# 任务1:按地区和产品的销售透视表
report1 = sales_detail.pivot_table(
values='sales',
index='region',
columns='product',
aggfunc='sum',
margins=True
)
print("地区×产品销售额:")
print(report1)
# 任务2:按月份和渠道的多指标透视
report2 = sales_detail.pivot_table(
values=['sales', 'cost'],
index='month',
columns='channel',
aggfunc={'sales': 'sum', 'cost': 'sum'}
)
# 计算利润
report2['利润率'] = ((report2['sales'] - report2['cost']) / report2['sales'] * 100).round(2)
print("\n月度渠道分析:")
print(report2)
# 任务3:多维透视(地区+产品+渠道)
report3 = sales_detail.pivot_table(
values='sales',
index=['region', 'product'],
columns='channel',
aggfunc='sum',
fill_value=0
)
report3['总计'] = report3.sum(axis=1)
print("\n多维销售透视:")
print(report3)
# 任务4:生成交叉表(crosstab)
cross_tab = pd.crosstab(
index=[sales_detail['region'], sales_detail['product']],
columns=sales_detail['channel'],
values=sales_detail['sales'],
aggfunc='sum',
margins=True,
normalize='all' # 显示占比
)
print("\n交叉占比分析:")
print((cross_tab * 100).round(2))
# 任务5:从透视表还原明细
# 使用reset_index将多层索引展开
restored = report3.reset_index()
melted = pd.melt(
restored,
id_vars=['region', 'product'],
value_vars=['Online', 'Offline'],
var_name='channel',
value_name='sales'
)
print("\n还原长格式:")
print(melted.head())
透视表美化输出:
# 带样式的透视表
pivot_styled = sales_detail.pivot_table(
values='sales',
index='region',
columns='product',
aggfunc='sum'
).style\
.background_gradient(cmap='YlGnBu')\
.format('{:.0f}')\
.set_caption('销售额热力图')
# 导出为HTML
pivot_styled.to_html('sales_report.html')
# 导出为Excel(保留格式)
with pd.ExcelWriter('sales_report.xlsx', engine='openpyxl') as writer:
pivot_styled.to_excel(writer, sheet_name='销售透视')
▶ 如何进行时间序列处理?
时间序列是数据分析中的重要场景,Pandas提供了强大的日期时间处理功能。
日期时间创建与转换:
import pandas as pd
import numpy as np
# 创建日期时间
date1 = pd.Timestamp('2024-01-01')
date2 = pd.Timestamp('2024-01-01 12:30:45')
date3 = pd.to_datetime('2024/01/01')
# 字符串转日期
df = pd.DataFrame({
'date_str': ['2024-01-01', '2024-01-02', '2024-01-03'],
'value': [100, 150, 120]
})
df['date'] = pd.to_datetime(df['date_str'])
# 多种格式转换
dates = pd.to_datetime(['2024-01-01', '01/02/2024', '2024.01.03'],
format='mixed')
# Unix时间戳转换
timestamps = [1704067200, 1704153600, 1704240000]
dates_from_ts = pd.to_datetime(timestamps, unit='s')
# 日期范围生成
date_range1 = pd.date_range('2024-01-01', '2024-01-31', freq='D') # 按天
date_range2 = pd.date_range('2024-01-01', periods=12, freq='MS') # 按月初
date_range3 = pd.date_range('2024-01-01', periods=24, freq='H') # 按小时
date_range4 = pd.bdate_range('2024-01-01', periods=20) # 工作日
日期时间属性提取:
# 创建带日期的DataFrame
df = pd.DataFrame({
'datetime': pd.date_range('2024-01-01', periods=100, freq='6H'),
'value': np.random.randint(100, 200, 100)
})
# 提取各部分
df['year'] = df['datetime'].dt.year
df['month'] = df['datetime'].dt.month
df['day'] = df['datetime'].dt.day
df['hour'] = df['datetime'].dt.hour
df['dayofweek'] = df['datetime'].dt.dayofweek # 0=周一
df['day_name'] = df['datetime'].dt.day_name() # Monday
df['quarter'] = df['datetime'].dt.quarter
df['is_month_end'] = df['datetime'].dt.is_month_end
df['is_weekend'] = df['datetime'].dt.dayofweek >= 5
# 周期处理
df['year_month'] = df['datetime'].dt.to_period('M')
df['year_week'] = df['datetime'].dt.to_period('W')
print(df.head())
时间序列操作:
# 设置日期为索引
df = df.set_index('datetime')
# 日期范围筛选
df_jan = df['2024-01'] # 2024年1月
df_range = df['2024-01-05':'2024-01-10'] # 日期范围
df_specific = df.loc['2024-01-01 06:00':'2024-01-01 18:00']
# 重采样(Resample)
daily_avg = df['value'].resample('D').mean() # 按天聚合平均
weekly_sum = df['value'].resample('W').sum() # 按周聚合求和
monthly_stats = df['value'].resample('MS').agg(['sum', 'mean', 'max', 'min'])
# 前向填充(工作日到日历日)
daily_data = df['value'].resample('D').asfreq()
daily_filled = daily_data.fillna(method='ffill')
# 移动窗口
df['MA_7'] = df['value'].rolling(window=7).mean() # 7天移动平均
df['MA_7_center'] = df['value'].rolling(window=7, center=True).mean()
# 指数加权移动平均
df['EMA_7'] = df['value'].ewm(span=7).mean()
# 差分
df['diff_1'] = df['value'].diff(1) # 一阶差分
df['pct_change'] = df['value'].pct_change() # 百分比变化
# 时间偏移
df['next_day'] = df['value'].shift(-1) # 前移(看未来)
df['prev_day'] = df['value'].shift(1) # 后移(看过去)
实战案例:股票数据分析:
# 模拟股票数据
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=252, freq='B') # 一年交易日
stock_df = pd.DataFrame({
'date': dates,
'open': 100 + np.random.randn(252).cumsum(),
'close': 100 + np.random.randn(252).cumsum(),
'high': 105 + np.random.randn(252).cumsum(),
'low': 95 + np.random.randn(252).cumsum(),
'volume': np.random.randint(1000000, 10000000, 252)
})
stock_df['close'] = stock_df['close'].clip(lower=50) # 确保价格为正
stock_df = stock_df.set_index('date')
# 计算技术指标
# 1. 移动平均线
stock_df['MA5'] = stock_df['close'].rolling(window=5).mean()
stock_df['MA10'] = stock_df['close'].rolling(window=10).mean()
stock_df['MA20'] = stock_df['close'].rolling(window=20).mean()
# 2. 布林带
stock_df['BB_middle'] = stock_df['close'].rolling(window=20).mean()
std_20 = stock_df['close'].rolling(window=20).std()
stock_df['BB_upper'] = stock_df['BB_middle'] + 2 * std_20
stock_df['BB_lower'] = stock_df['BB_middle'] - 2 * std_20
# 3. 日收益率
stock_df['daily_return'] = stock_df['close'].pct_change()
# 4. 累计收益率
stock_df['cumulative_return'] = (1 + stock_df['daily_return']).cumprod() - 1
# 5. 波动率(20日滚动标准差年化)
stock_df['volatility'] = stock_df['daily_return'].rolling(window=20).std() * np.sqrt(252)
# 6. RSI相对强弱指标
delta = stock_df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
stock_df['RSI'] = 100 - (100 / (1 + rs))
# 7. 成交量移动平均
stock_df['volume_MA20'] = stock_df['volume'].rolling(window=20).mean()
# 月度汇总统计
monthly_summary = stock_df.resample('MS').agg({
'open': 'first',
'close': 'last',
'high': 'max',
'low': 'min',
'volume': 'sum'
})
monthly_summary['return'] = monthly_summary['close'].pct_change()
print("最近20天数据:")
print(stock_df[['close', 'MA5', 'MA10', 'MA20', 'RSI']].tail(20))
print("\n月度汇总:")
print(monthly_summary)
时区处理:
# 创建带时区的日期
dates_utc = pd.date_range('2024-01-01', periods=24, freq='H', tz='UTC')
# 转换时区
dates_beijing = dates_utc.tz_convert('Asia/Shanghai')
dates_ny = dates_utc.tz_convert('America/New_York')
# DataFrame中的时区处理
df = pd.DataFrame({
'datetime_utc': dates_utc,
'value': range(24)
})
df['datetime_beijing'] = df['datetime_utc'].dt.tz_convert('Asia/Shanghai')
Pandas性能优化
▶ 如何优化Pandas代码性能?
性能优化对于处理大数据集至关重要,需要从多个角度优化代码。
向量化操作:
import pandas as pd
import numpy as np
import time
# 创建测试数据
df = pd.DataFrame({
'A': np.random.randint(0, 100, 1000000),
'B': np.random.randint(0, 100, 1000000)
})
# 方法1:循环(慢,不推荐)
start = time.time()
result = []
for i in range(len(df)):
result.append(df.iloc[i]['A'] + df.iloc[i]['B'])
df['C_loop'] = result
print(f"循环耗时: {time.time() - start:.3f}秒")
# 方法2:向量化(快,推荐)
start = time.time()
df['C_vectorized'] = df['A'] + df['B']
print(f"向量化耗时: {time.time() - start:.3f}秒")
# 方法3:NumPy操作(更快)
start = time.time()
df['C_numpy'] = np.add(df['A'].values, df['B'].values)
print(f"NumPy耗时: {time.time() - start:.3f}秒")
避免使用apply的场景:
# 慢:使用apply
df['result_slow'] = df.apply(lambda x: x['A'] * 2 + x['B'] * 3, axis=1)
# 快:向量化
df['result_fast'] = df['A'] * 2 + df['B'] * 3
# 条件计算
# 慢
df['category_slow'] = df['A'].apply(lambda x: 'High' if x > 50 else 'Low')
# 快
df['category_fast'] = np.where(df['A'] > 50, 'High', 'Low')
# 或使用cut
df['category_cut'] = pd.cut(df['A'], bins=[0, 50, 100], labels=['Low', 'High'])
数据类型优化:
# 检查当前内存占用
print(f"原始内存: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
# 优化整数类型
df['A'] = df['A'].astype('int8') # 0-255范围可用int8
df['B'] = df['B'].astype('int16') # 更大范围用int16
# 优化浮点类型
df_float = pd.DataFrame({'col': np.random.rand(1000000)})
df_float['col'] = df_float['col'].astype('float32') # float64→float32
# 分类类型(重复值多)
df_cat = pd.DataFrame({
'city': np.random.choice(['Beijing', 'Shanghai', 'Guangzhou'], 100000)
})
print(f"object类型: {df_cat.memory_usage(deep=True).sum() / 1024:.2f} KB")
df_cat['city'] = df_cat['city'].astype('category')
print(f"category类型: {df_cat.memory_usage(deep=True).sum() / 1024:.2f} KB")
# 批量优化函数
def optimize_dtypes(df):
"""
自动优化DataFrame的数据类型
"""
for col in df.columns:
col_type = df[col].dtype
if col_type != 'object':
c_min = df[col].min()
c_max = df[col].max()
if str(col_type)[:3] == 'int':
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
df[col] = df[col].astype(np.int32)
else:
if c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
df[col] = df[col].astype(np.float32)
else:
# 字符串列,检查是否适合category
if df[col].nunique() / len(df) < 0.5: # 唯一值<50%
df[col] = df[col].astype('category')
return df
df_optimized = optimize_dtypes(df.copy())
print(f"优化后内存: {df_optimized.memory_usage(deep=True).sum() / 1024**2:.2f} MB")
分块处理大文件:
# 分块读取
def process_large_file(filename, chunksize=100000):
"""
分块处理大文件
"""
results = []
for chunk in pd.read_csv(filename, chunksize=chunksize):
# 对每个chunk进行处理
chunk_filtered = chunk[chunk['value'] > 100]
chunk_processed = chunk_filtered.groupby('category')['value'].sum()
results.append(chunk_processed)
# 合并结果
final_result = pd.concat(results).groupby(level=0).sum()
return final_result
# 使用Dask处理超大数据
import dask.dataframe as dd
# 读取大文件
dask_df = dd.read_csv('large_file.csv')
# Dask操作(惰性执行)
result = dask_df.groupby('category')['value'].mean()
# 触发计算
computed_result = result.compute()
索引优化:
# 设置索引加速查询
df_large = pd.DataFrame({
'id': range(1000000),
'value': np.random.rand(1000000)
})
# 无索引查询(慢)
start = time.time()
result = df_large[df_large['id'] == 500000]
print(f"无索引查询: {time.time() - start:.4f}秒")
# 设置索引(快)
df_indexed = df_large.set_index('id')
start = time.time()
result = df_indexed.loc[500000]
print(f"有索引查询: {time.time() - start:.4f}秒")
# 多列索引
df_multi = pd.DataFrame({
'A': np.repeat(['X', 'Y', 'Z'], 100000),
'B': np.tile(range(100000), 3),
'C': np.random.rand(300000)
})
df_multi_indexed = df_multi.set_index(['A', 'B'])
# 查询更快
result = df_multi_indexed.loc[('X', 50000)]
并行处理:
from multiprocessing import Pool
import pandas as pd
def process_group(args):
"""
处理单个分组
"""
name, group = args
# 复杂计算
result = group['value'].apply(lambda x: x ** 2 + np.log(x + 1))
return name, result.sum()
def parallel_groupby(df, n_cores=4):
"""
并行分组处理
"""
groups = list(df.groupby('category'))
with Pool(n_cores) as pool:
results = pool.map(process_group, groups)
return dict(results)
# 使用Pandas内置并行(需要Python 3.8+)
# 注意:实际效果取决于操作类型
df_result = df.groupby('category').parallel_apply(lambda x: x['value'].sum())
高效合并:
# 大表merge优化
# 1. 提前过滤
df1_filtered = df1[df1['date'] >= '2024-01-01']
merged = pd.merge(df1_filtered, df2, on='key')
# 2. 使用category类型
df1['key'] = df1['key'].astype('category')
df2['key'] = df2['key'].astype('category')
# 3. 避免重复merge
# 慢
result = df1
for df in [df2, df3, df4]:
result = pd.merge(result, df, on='key')
# 快
from functools import reduce
dfs = [df1, df2, df3, df4]
result = reduce(lambda left, right: pd.merge(left, right, on='key'), dfs)
性能分析工具:
# 使用%%timeit魔法命令(Jupyter)
# %%timeit
# df['result'] = df['A'] * 2
# 使用line_profiler
# %load_ext line_profiler
# %lprun -f function_name function_name(args)
# 内存分析
from memory_profiler import profile
@profile
def process_data(df):
df['new_col'] = df['A'] * df['B']
result = df.groupby('category')['new_col'].sum()
return result
# Pandas自带性能测试
import pandas as pd
# 查看操作耗时
with pd.option_context('mode.chained_assignment', None):
# 你的代码
pass
最佳实践总结:
- 向量化优先:避免循环,使用NumPy操作
- 数据类型优化:使用合适的数据类型(int8/int16/category)
- 分块处理:大文件分chunk读取
- 索引利用:频繁查询的列设置为索引
- 避免链式赋值:使用
.loc显式赋值 - 合理使用apply:能向量化的不用apply
- 并行计算:利用多核处理
- 提前过滤:先筛选再操作
▶ 如何进行字符串处理?
Pandas提供了强大的字符串处理功能,通过.str访问器实现。
基础字符串操作:
import pandas as pd
# 示例数据
df = pd.DataFrame({
'name': [' Alice ', 'bob', 'CHARLIE', 'David'],
'email': ['alice@gmail.com', 'bob@outlook.com', 'charlie@qq.com', 'david@163.com'],
'phone': ['138-0000-0000', '139-1111-1111', '186-2222-2222', '187-3333-3333']
})
# 大小写转换
df['name_upper'] = df['name'].str.upper()
df['name_lower'] = df['name'].str.lower()
df['name_title'] = df['name'].str.title() # 首字母大写
df['name_capitalize'] = df['name'].str.capitalize() # 只第一个字母大写
# 去除空格
df['name_strip'] = df['name'].str.strip() # 两端
df['name_lstrip'] = df['name'].str.lstrip() # 左侧
df['name_rstrip'] = df['name'].str.rstrip() # 右侧
# 替换
df['phone_clean'] = df['phone'].str.replace('-', '')
df['email_domain'] = df['email'].str.replace(r'.*@', '', regex=True)
# 长度
df['name_length'] = df['name'].str.len()
# 切片
df['first_name'] = df['name'].str[:5]
df['last_char'] = df['name'].str[-1]
print(df)
字符串检查与判断:
# 包含判断
df['is_gmail'] = df['email'].str.contains('gmail')
df['has_138'] = df['phone'].str.contains('138')
# 开头/结尾判断
df['starts_with_A'] = df['name'].str.startswith('A', na=False)
df['ends_with_com'] = df['email'].str.endswith('.com')
# 正则匹配
df['valid_phone'] = df['phone'].str.match(r'^\d{3}-\d{4}-\d{4}$')
# 查找位置
df['at_position'] = df['email'].str.find('@') # 返回位置,找不到返回-1
df['at_index'] = df['email'].str.index('@') # 返回位置,找不到报错
# 计数
df['digit_count'] = df['phone'].str.count(r'\d')
字符串拆分与提取:
# 拆分为列表
df['email_parts'] = df['email'].str.split('@')
# 拆分为多列
df[['username', 'domain']] = df['email'].str.split('@', expand=True)
# 按多个分隔符拆分
df['phone_parts'] = df['phone'].str.split(r'[-/]', expand=False)
# 提取(正则)
df['area_code'] = df['phone'].str.extract(r'^(\d{3})')
df['email_provider'] = df['email'].str.extract(r'@(\w+)\.')
# 提取所有匹配(返回DataFrame)
df['all_numbers'] = df['phone'].str.findall(r'\d+')
# 获取指定位置的元素(拆分后)
df['first_digit'] = df['phone'].str.split('-').str[0]
实战案例:数据清洗:
# 场景:清洗用户数据
user_df = pd.DataFrame({
'name': [' 张三 ', 'li si', 'WANG五', '赵六 '],
'mobile': ['138 0000 0000', '139-1111-1111', '13622222222', '187 3333 3333'],
'id_card': ['110101199001011234', '310101198512125678', None, '440101197703033456'],
'address': ['北京市朝阳区xx路100号', '上海市浦东新区yy街200号', '广州市天河区zz大道300号', None]
})
# 1. 清洗姓名:去空格、统一格式
user_df['name_clean'] = user_df['name'].str.strip().str.title()
# 2. 清洗手机号:去除所有非数字字符
user_df['mobile_clean'] = user_df['mobile'].str.replace(r'\D', '', regex=True)
# 3. 验证手机号格式
user_df['mobile_valid'] = user_df['mobile_clean'].str.match(r'^1[3-9]\d{9}$')
# 4. 从身份证提取信息
user_df['birth_year'] = user_df['id_card'].str[6:10]
user_df['birth_month'] = user_df['id_card'].str[10:12]
user_df['birth_day'] = user_df['id_card'].str[12:14]
user_df['gender'] = user_df['id_card'].str[-2:-1].astype(float) % 2 # 1=男, 0=女
# 5. 从地址提取城市
user_df['city'] = user_df['address'].str.extract(r'(\w+市)')
# 6. 生成标准格式手机号
user_df['mobile_formatted'] = user_df['mobile_clean'].str.replace(
r'(\d{3})(\d{4})(\d{4})',
r'\1-\2-\3',
regex=True
)
print(user_df)
高级字符串操作:
# 填充
df['name_padded'] = df['name'].str.pad(width=10, fillchar='*') # 右填充
df['name_center'] = df['name'].str.center(width=10, fillchar='-') # 居中填充
df['name_zfill'] = df['name'].str.zfill(10) # 左侧补0
# 重复
df['name_repeat'] = df['name'].str.repeat(3)
# 翻译(类似str.translate)
translation_table = str.maketrans('abc', '123')
df['name_translate'] = df['name'].str.translate(translation_table)
# 正则替换(复杂规则)
def mask_phone(x):
if pd.isna(x):
return x
return x[:3] + '****' + x[-4:]
df['phone_masked'] = df['phone_clean'].apply(mask_phone)
# 或使用正则
df['phone_masked'] = df['phone_clean'].str.replace(
r'(\d{3})\d{4}(\d{4})',
r'\1****\2',
regex=True
)
# 格式化
df['formatted'] = df.apply(
lambda row: f"{row['name']} ({row['email']})",
axis=1
)
性能优化:
# 对于大数据集,某些操作可以用Python内置方法加速
# 慢
df['result'] = df['text'].str.lower()
# 快
df['result'] = df['text'].str.lower() # str访问器已经优化,差别不大
# 但对于简单操作,map可能更快
df['result'] = df['text'].map(str.lower)
# 或使用向量化的NumPy字符串函数
import numpy as np
df['result'] = np.char.lower(df['text'].values.astype(str))
▶ 如何处理日期时间数据?
日期时间处理是时间序列分析的基础,Pandas提供了丰富的功能。
日期转换与创建:
import pandas as pd
import numpy as np
# 字符串转日期
df = pd.DataFrame({
'date_str': ['2024-01-01', '2024/01/02', '01-03-2024', '2024年1月4日']
})
# 自动识别格式
df['date1'] = pd.to_datetime(df['date_str'], errors='coerce')
# 指定格式
df['date2'] = pd.to_datetime(df['date_str'], format='%Y-%m-%d', errors='coerce')
# 混合格式
df['date3'] = pd.to_datetime(df['date_str'], format='mixed', errors='coerce')
# 从多列创建日期
df_parts = pd.DataFrame({
'year': [2024, 2024, 2024],
'month': [1, 2, 3],
'day': [15, 20, 25]
})
df_parts['date'] = pd.to_datetime(df_parts[['year', 'month', 'day']])
# Unix时间戳转换
timestamps = [1704067200, 1704153600, 1704240000]
df_ts = pd.DataFrame({'timestamp': timestamps})
df_ts['date'] = pd.to_datetime(df_ts['timestamp'], unit='s')
# 创建日期范围
date_range = pd.date_range('2024-01-01', '2024-12-31', freq='D') # 每天
month_range = pd.date_range('2024-01-01', periods=12, freq='MS') # 每月初
hour_range = pd.date_range('2024-01-01', periods=24, freq='H') # 每小时
business_days = pd.bdate_range('2024-01-01', periods=20) # 工作日
日期属性提取:
# 创建示例数据
df = pd.DataFrame({
'datetime': pd.date_range('2024-01-01', periods=100, freq='6H')
})
# 提取各部分
df['year'] = df['datetime'].dt.year
df['month'] = df['datetime'].dt.month
df['day'] = df['datetime'].dt.day
df['hour'] = df['datetime'].dt.hour
df['minute'] = df['datetime'].dt.minute
df['second'] = df['datetime'].dt.second
# 星期相关
df['dayofweek'] = df['datetime'].dt.dayofweek # 0=周一
df['day_name'] = df['datetime'].dt.day_name() # Monday
df['weekday'] = df['datetime'].dt.weekday
df['dayofyear'] = df['datetime'].dt.dayofyear
# 季度和周
df['quarter'] = df['datetime'].dt.quarter
df['week'] = df['datetime'].dt.isocalendar().week
# 日期判断
df['is_month_start'] = df['datetime'].dt.is_month_start
df['is_month_end'] = df['datetime'].dt.is_month_end
df['is_quarter_start'] = df['datetime'].dt.is_quarter_start
df['is_quarter_end'] = df['datetime'].dt.is_quarter_end
df['is_year_start'] = df['datetime'].dt.is_year_start
df['is_year_end'] = df['datetime'].dt.is_year_end
df['is_leap_year'] = df['datetime'].dt.is_leap_year
# 自定义判断
df['is_weekend'] = df['datetime'].dt.dayofweek >= 5
df['is_morning'] = df['datetime'].dt.hour < 12
df['is_business_hour'] = df['datetime'].dt.hour.between(9, 17)
print(df.head(20))
日期计算:
# 日期加减
df['next_day'] = df['datetime'] + pd.Timedelta(days=1)
df['next_week'] = df['datetime'] + pd.Timedelta(weeks=1)
df['next_month'] = df['datetime'] + pd.DateOffset(months=1)
df['next_year'] = df['datetime'] + pd.DateOffset(years=1)
# 日期差值
df['days_since_start'] = (df['datetime'] - df['datetime'].min()).dt.days
df['days_to_end'] = (df['datetime'].max() - df['datetime']).dt.days
# 两个日期列的差值
df2 = pd.DataFrame({
'start_date': pd.date_range('2024-01-01', periods=10),
'end_date': pd.date_range('2024-01-05', periods=10)
})
df2['duration_days'] = (df2['end_date'] - df2['start_date']).dt.days
# 工作日计算
from pandas.tseries.offsets import BDay
df['next_business_day'] = df['datetime'] + BDay(1)
df['prev_business_day'] = df['datetime'] - BDay(1)
# 月末日期
from pandas.tseries.offsets import MonthEnd
df['month_end'] = df['datetime'] + MonthEnd(0) # 当月月末
df['next_month_end'] = df['datetime'] + MonthEnd(1) # 下月月末
时间序列聚合:
# 创建时间序列数据
np.random.seed(42)
ts_df = pd.DataFrame({
'datetime': pd.date_range('2024-01-01', periods=365, freq='D'),
'value': np.random.randint(100, 1000, 365)
})
ts_df = ts_df.set_index('datetime')
# 重采样
daily_mean = ts_df.resample('D').mean() # 按天(原本就是天)
weekly_sum = ts_df.resample('W').sum() # 按周聚合
monthly_stats = ts_df.resample('MS').agg(['sum', 'mean', 'max', 'min'])
# 前向/后向填充
weekly_ffill = ts_df.resample('W').ffill() # 前向填充
weekly_bfill = ts_df.resample('W').bfill() # 后向填充
# 降采样(Down sampling)
monthly_data = ts_df.resample('M').agg({
'value': ['sum', 'mean', 'std', 'count']
})
# 升采样(Up sampling)
hourly_data = ts_df.resample('H').asfreq() # 生成小时数据(缺失值为NaN)
hourly_filled = ts_df.resample('H').interpolate() # 插值填充
print("月度统计:")
print(monthly_data.head())
实战案例:用户行为分析:
# 场景:分析用户登录行为
login_df = pd.DataFrame({
'user_id': np.random.randint(1, 100, 1000),
'login_time': pd.date_range('2024-01-01', periods=1000, freq='37T'), # 每37分钟一条
'device': np.random.choice(['iOS', 'Android', 'Web'], 1000)
})
# 1. 提取时间特征
login_df['date'] = login_df['login_time'].dt.date
login_df['hour'] = login_df['login_time'].dt.hour
login_df['day_of_week'] = login_df['login_time'].dt.day_name()
login_df['is_weekend'] = login_df['login_time'].dt.dayofweek >= 5
# 2. 按小时统计登录量
hourly_logins = login_df.groupby(login_df['login_time'].dt.hour).size()
print("各小时登录量:")
print(hourly_logins)
# 3. 按日期和设备统计
daily_device = login_df.groupby([
login_df['login_time'].dt.date,
'device'
]).size().unstack(fill_value=0)
print("\n每日各设备登录量:")
print(daily_device.head())
# 4. 计算用户活跃度(按周)
login_df['week'] = login_df['login_time'].dt.to_period('W')
weekly_active_users = login_df.groupby('week')['user_id'].nunique()
print("\n每周活跃用户数:")
print(weekly_active_users)
# 5. 计算用户登录间隔
user_intervals = login_df.sort_values(['user_id', 'login_time'])
user_intervals['prev_login'] = user_intervals.groupby('user_id')['login_time'].shift(1)
user_intervals['interval_hours'] = (
user_intervals['login_time'] - user_intervals['prev_login']
).dt.total_seconds() / 3600
# 6. 识别高峰时段
peak_hours = login_df.groupby(login_df['login_time'].dt.hour).size().nlargest(3)
print("\n登录高峰时段:")
print(peak_hours)
时区处理:
# 创建带时区的时间
df_tz = pd.DataFrame({
'datetime_utc': pd.date_range('2024-01-01', periods=24, freq='H', tz='UTC')
})
# 转换时区
df_tz['datetime_beijing'] = df_tz['datetime_utc'].dt.tz_convert('Asia/Shanghai')
df_tz['datetime_ny'] = df_tz['datetime_utc'].dt.tz_convert('America/New_York')
# 去除时区信息
df_tz['datetime_naive'] = df_tz['datetime_utc'].dt.tz_localize(None)
# 为无时区时间添加时区
df_naive = pd.DataFrame({
'datetime': pd.date_range('2024-01-01', periods=10)
})
df_naive['datetime_utc'] = df_naive['datetime'].dt.tz_localize('UTC')
▶ 如何进行数据去重?
数据去重是数据清洗的重要环节,需要根据业务规则选择合适的策略。
基础去重:
import pandas as pd
import numpy as np
# 创建包含重复的示例数据
df = pd.DataFrame({
'user_id': [1, 2, 2, 3, 3, 3, 4],
'name': ['Alice', 'Bob', 'Bob', 'Charlie', 'Charlie', 'Charlie', 'David'],
'age': [25, 30, 30, 35, 35, 36, 28], # 注意第三个Charlie年龄不同
'city': ['Beijing', 'Shanghai', 'Shanghai', 'Beijing', 'Beijing', 'Beijing', 'Guangzhou']
})
# 查看重复行
print("完全重复的行:")
print(df[df.duplicated()])
# 查看重复行(包括第一次出现)
print("\n所有重复值(包括首次):")
print(df[df.duplicated(keep=False)])
# 统计重复数量
print(f"\n重复行数量:{df.duplicated().sum()}")
# 删除完全重复的行
df_dedupe = df.drop_duplicates()
print("\n删除重复后:")
print(df_dedupe)
按列去重:
# 按指定列去重
df_dedupe_user = df.drop_duplicates(subset=['user_id'])
print("按user_id去重:")
print(df_dedupe_user)
# 按多列去重
df_dedupe_multi = df.drop_duplicates(subset=['name', 'age'])
print("\n按name和age去重:")
print(df_dedupe_multi)
# 保留策略
df_keep_first = df.drop_duplicates(subset=['user_id'], keep='first') # 保留第一个
df_keep_last = df.drop_duplicates(subset=['user_id'], keep='last') # 保留最后一个
df_keep_none = df.drop_duplicates(subset=['user_id'], keep=False) # 全部删除
print("\n保留第一个:")
print(df_keep_first)
print("\n保留最后一个:")
print(df_keep_last)
print("\n全部删除:")
print(df_keep_none)
高级去重策略:
# 场景:订单表,保留金额最大的订单
orders_df = pd.DataFrame({
'order_id': [1, 2, 3, 4, 5, 6],
'user_id': [1, 1, 2, 2, 3, 3],
'amount': [100, 150, 200, 180, 90, 120],
'order_date': pd.date_range('2024-01-01', periods=6)
})
# 方法1:先排序,再去重(保留金额最大的)
orders_sorted = orders_df.sort_values('amount', ascending=False)
orders_dedupe = orders_sorted.drop_duplicates(subset=['user_id'], keep='first')
print("保留金额最大的订单:")
print(orders_dedupe)
# 方法2:使用groupby + idxmax
max_amount_idx = orders_df.groupby('user_id')['amount'].idxmax()
orders_max = orders_df.loc[max_amount_idx]
print("\n使用groupby保留最大值:")
print(orders_max)
# 方法3:保留最新的记录
latest_idx = orders_df.groupby('user_id')['order_date'].idxmax()
orders_latest = orders_df.loc[latest_idx]
print("\n保留最新订单:")
print(orders_latest)
复杂去重规则:
# 场景:用户登录记录,需要去除5分钟内的重复登录
login_df = pd.DataFrame({
'user_id': [1, 1, 1, 2, 2, 3, 3, 3],
'login_time': pd.to_datetime([
'2024-01-01 10:00:00',
'2024-01-01 10:02:00', # 2分钟后,应去重
'2024-01-01 10:10:00', # 10分钟后,保留
'2024-01-01 11:00:00',
'2024-01-01 11:03:00', # 3分钟后,应去重
'2024-01-01 12:00:00',
'2024-01-01 12:04:00', # 4分钟后,应去重
'2024-01-01 12:15:00', # 15分钟后,保留
])
})
# 按用户排序
login_df = login_df.sort_values(['user_id', 'login_time'])
# 计算与上次登录的时间差
login_df['time_diff'] = login_df.groupby('user_id')['login_time'].diff()
# 标记是否应保留(第一条或时间差>5分钟)
login_df['keep'] = (login_df['time_diff'].isna()) | (login_df['time_diff'] > pd.Timedelta(minutes=5))
# 过滤
login_deduped = login_df[login_df['keep']].drop(columns=['time_diff', 'keep'])
print("去除5分钟内重复登录:")
print(login_deduped)
模糊去重(相似性去重):
# 场景:公司名称可能有细微差异
companies_df = pd.DataFrame({
'company': [
'北京科技有限公司',
'北京科技有限公司 ', # 多了空格
'北京科技有限责任公司', # 略有不同
'上海贸易公司',
'上海贸易有限公司',
]
})
# 方法1:去除空格后比较
companies_df['company_clean'] = companies_df['company'].str.strip()
companies_dedupe1 = companies_df.drop_duplicates(subset=['company_clean'])
# 方法2:使用fuzzywuzzy进行模糊匹配
from fuzzywuzzy import fuzz
def fuzzy_dedupe(df, column, threshold=90):
"""
基于相似度的去重
"""
keep_indices = []
processed = set()
for idx, value in df[column].items():
if idx in processed:
continue
keep_indices.append(idx)
processed.add(idx)
# 查找相似的行
for idx2, value2 in df[column].items():
if idx2 not in processed:
similarity = fuzz.ratio(str(value), str(value2))
if similarity >= threshold:
processed.add(idx2)
return df.loc[keep_indices]
# companies_fuzzy = fuzzy_dedupe(companies_df, 'company', threshold=85)
# print("模糊去重后:")
# print(companies_fuzzy)
标记重复而不删除:
# 场景:需要保留所有数据,但标记哪些是重复的
df_with_flag = df.copy()
# 标记是否重复
df_with_flag['is_duplicate'] = df_with_flag.duplicated(subset=['user_id'], keep=False)
# 添加重复组编号
df_with_flag['duplicate_group'] = df_with_flag.groupby('user_id').ngroup()
# 标记是第几个重复
df_with_flag['duplicate_rank'] = df_with_flag.groupby('user_id').cumcount() + 1
print("标记重复信息:")
print(df_with_flag)
去重统计报告:
def dedup_report(df, subset=None):
"""
生成去重报告
"""
total_rows = len(df)
if subset:
duplicates = df.duplicated(subset=subset, keep=False)
unique_rows = len(df.drop_duplicates(subset=subset))
else:
duplicates = df.duplicated(keep=False)
unique_rows = len(df.drop_duplicates())
duplicate_rows = duplicates.sum()
duplicate_rate = (duplicate_rows / total_rows * 100) if total_rows > 0 else 0
report = pd.DataFrame({
'指标': ['总行数', '唯一行数', '重复行数', '重复率(%)'],
'数值': [total_rows, unique_rows, duplicate_rows, f'{duplicate_rate:.2f}']
})
return report
# 使用示例
print(dedup_report(df, subset=['user_id']))
批量去重(大数据):
# 对于超大DataFrame,分块去重
def chunk_dedupe(df, chunksize=10000, subset=None):
"""
分块去重,适用于内存不足的情况
"""
unique_df = pd.DataFrame()
for start in range(0, len(df), chunksize):
chunk = df.iloc[start:start+chunksize]
# 当前chunk去重
chunk_unique = chunk.drop_duplicates(subset=subset)
# 与已有结果合并后再去重
unique_df = pd.concat([unique_df, chunk_unique])
unique_df = unique_df.drop_duplicates(subset=subset)
return unique_df
# 使用示例
# large_df_deduped = chunk_dedupe(large_df, chunksize=10000, subset=['user_id'])
SQL查询基础
▶ 如何查询每个部门的平均薪资?
这是典型的分组聚合问题,考察GROUP BY和聚合函数的使用。
场景:有员工表employees(id, name, department, salary),需要统计每个部门的平均薪资。
-- 基础查询
SELECT
department,
AVG(salary) as avg_salary
FROM employees
GROUP BY department;
-- 进阶:筛选平均薪资大于8000的部门
SELECT
department,
AVG(salary) as avg_salary,
COUNT(*) as emp_count
FROM employees
GROUP BY department
HAVING AVG(salary) > 8000
ORDER BY avg_salary DESC;
-- 高级:包含部门人数和薪资范围
SELECT
department,
COUNT(*) as emp_count,
ROUND(AVG(salary), 2) as avg_salary,
MIN(salary) as min_salary,
MAX(salary) as max_salary,
MAX(salary) - MIN(salary) as salary_range
FROM employees
GROUP BY department
ORDER BY avg_salary DESC;
关键点:
GROUP BY后面的列必须在SELECT中出现(或使用聚合函数)HAVING用于筛选聚合后的结果,WHERE用于筛选原始数据- 聚合函数:
COUNT(),SUM(),AVG(),MIN(),MAX()
执行顺序
▶ 如何找出每个部门薪资最高的员工?
这是经典的"分组取Top N"问题,有多种解决方案。
场景:员工表employees(id, name, department, salary),找出每个部门薪资最高的员工。
方法1:相关子查询
-- 思路:对每个员工,检查是否是本部门最高薪资
SELECT e1.department, e1.name, e1.salary
FROM employees e1
WHERE e1.salary = (
SELECT MAX(e2.salary)
FROM employees e2
WHERE e2.department = e1.department
);
优点:逻辑清晰,易理解 缺点:性能较差(每行都要执行子查询)
方法2:窗口函数(推荐)
-- 使用ROW_NUMBER排名
SELECT department, name, salary
FROM (
SELECT
department,
name,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rn
FROM employees
) t
WHERE rn = 1;
-- 使用RANK(可能有并列第一)
SELECT department, name, salary
FROM (
SELECT
department,
name,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rk
FROM employees
) t
WHERE rk = 1;
-- 使用DENSE_RANK
SELECT department, name, salary
FROM (
SELECT
department,
name,
salary,
DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) as drk
FROM employees
) t
WHERE drk = 1;
三种排名函数的区别:
| 薪资 | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 9000 | 1 | 1 | 1 |
| 9000 | 2 | 1 | 1 |
| 8000 | 3 | 3 | 2 |
| 8000 | 4 | 3 | 2 |
| 7000 | 5 | 5 | 3 |
特点对比:
ROW_NUMBER:连续递增,每行唯一编号RANK:相同值并列排名,后续排名跳号DENSE_RANK:相同值并列排名,后续排名不跳号
方法3:自连接
SELECT e1.department, e1.name, e1.salary
FROM employees e1
LEFT JOIN employees e2
ON e1.department = e2.department
AND e1.salary < e2.salary
WHERE e2.id IS NULL;
思路:左连接找比自己薪资高的同部门员工,如果没有(IS NULL),说明自己最高
扩展:取前N名
-- 每个部门薪资前3的员工
SELECT department, name, salary, rn
FROM (
SELECT
department,
name,
salary,
ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rn
FROM employees
) t
WHERE rn <= 3
ORDER BY department, rn;
▶ 如何计算用户的连续登录天数?
这是典型的连续问题,考察日期处理和分组技巧。
场景:用户登录表user_login(user_id, login_date),计算每个用户的最长连续登录天数。
方法1:日期差值分组法
核心思想:连续日期减去行号,结果相同的为一组连续日期。
-- Step 1: 去重并排序
WITH login_data AS (
SELECT DISTINCT user_id, login_date
FROM user_login
),
-- Step 2: 添加行号
ranked_data AS (
SELECT
user_id,
login_date,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) as rn
FROM login_data
),
-- Step 3: 计算日期差(相同差值=连续)
grouped_data AS (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL rn DAY) as group_date
FROM ranked_data
)
-- Step 4: 统计每组的连续天数
SELECT
user_id,
MAX(consecutive_days) as max_consecutive_days
FROM (
SELECT
user_id,
group_date,
COUNT(*) as consecutive_days
FROM grouped_data
GROUP BY user_id, group_date
) t
GROUP BY user_id;
原理示例:
user_id login_date rn login_date - rn
1 2024-01-01 1 2023-12-31 (组A)
1 2024-01-02 2 2023-12-31 (组A)
1 2024-01-03 3 2023-12-31 (组A)
1 2024-01-05 4 2024-01-01 (组B)
1 2024-01-06 5 2024-01-01 (组B)
组A有3天连续,组B有2天连续。
方法2:窗口函数LAG
WITH lag_data AS (
SELECT
user_id,
login_date,
LAG(login_date, 1) OVER (PARTITION BY user_id ORDER BY login_date) as prev_date
FROM (SELECT DISTINCT user_id, login_date FROM user_login) t
),
-- 标记是否连续
flag_data AS (
SELECT
user_id,
login_date,
CASE
WHEN DATEDIFF(login_date, prev_date) = 1 THEN 0
ELSE 1
END as is_new_streak
FROM lag_data
),
-- 累计标记,生成分组ID
streak_groups AS (
SELECT
user_id,
login_date,
SUM(is_new_streak) OVER (PARTITION BY user_id ORDER BY login_date) as streak_id
FROM flag_data
)
-- 统计每组天数
SELECT
user_id,
MAX(consecutive_days) as max_consecutive_days
FROM (
SELECT
user_id,
streak_id,
COUNT(*) as consecutive_days
FROM streak_groups
GROUP BY user_id, streak_id
) t
GROUP BY user_id;
方法3:变量法(MySQL特定)
SET @prev_user = NULL;
SET @prev_date = NULL;
SET @streak_id = 0;
SELECT
user_id,
MAX(consecutive_days) as max_consecutive_days
FROM (
SELECT
user_id,
streak_id,
COUNT(*) as consecutive_days
FROM (
SELECT
user_id,
login_date,
@streak_id := CASE
WHEN @prev_user = user_id AND DATEDIFF(login_date, @prev_date) = 1
THEN @streak_id
ELSE @streak_id + 1
END as streak_id,
@prev_user := user_id,
@prev_date := login_date
FROM (
SELECT DISTINCT user_id, login_date
FROM user_login
ORDER BY user_id, login_date
) t
) t2
GROUP BY user_id, streak_id
) t3
GROUP BY user_id;
实际应用场景:
- 用户活跃度分析
- 连续签到统计
- 留存率计算
- 游戏连胜记录
▶ 如何进行多表关联查询?
多表JOIN是SQL的核心,需要理解不同JOIN类型的差异。
场景:
users(user_id, name)orders(order_id, user_id, amount, order_date)products(product_id, product_name, price)order_items(order_id, product_id, quantity)
JOIN类型对比:
| JOIN类型 | 说明 | 返回结果 | 使用场景 | 示例 |
|---|---|---|---|---|
| INNER JOIN | 内连接 | 只返回两表都匹配的记录 | 查询有订单的用户 | 用户-订单(只看下过单的) |
| LEFT JOIN | 左连接 | 返回左表所有记录 右表无匹配显示NULL | 查询所有用户及其订单 (包括未下单用户) | 用户列表-订单(可能无订单) |
| RIGHT JOIN | 右连接 | 返回右表所有记录 左表无匹配显示NULL | 查询所有订单及其用户 (包括无用户的订单) | 用户-订单列表(重点看订单) |
| FULL OUTER JOIN | 全外连接 | 返回两表所有记录 无匹配的显示NULL | 查询所有用户和订单 (MySQL需用UNION模拟) | 完整的用户-订单关系 |
| CROSS JOIN | 交叉连接 | 笛卡尔积(所有组合) | 生成所有可能的组合 | 日期表×产品表(生成排期) |
| SELF JOIN | 自连接 | 表与自身连接 | 查询层级关系、对比 | 员工-上级、同部门员工对比 |
代码示例:
-- 1. INNER JOIN(内连接):只返回两表都有的记录
SELECT u.name, o.order_id, o.amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id;
-- 2. LEFT JOIN(左连接):返回左表所有记录
SELECT u.name, o.order_id, o.amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;
-- 3. RIGHT JOIN(右连接):返回右表所有记录
SELECT u.name, o.order_id, o.amount
FROM users u
RIGHT JOIN orders o ON u.user_id = o.user_id;
-- 4. FULL OUTER JOIN(全外连接):MySQL需要UNION模拟
SELECT u.name, o.order_id, o.amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
UNION
SELECT u.name, o.order_id, o.amount
FROM users u
RIGHT JOIN orders o ON u.user_id = o.user_id;
复杂关联示例:
-- 查询每个用户的订单数、总消费、购买的商品数
SELECT
u.user_id,
u.name,
COUNT(DISTINCT o.order_id) as order_count,
COALESCE(SUM(o.amount), 0) as total_amount,
COUNT(DISTINCT oi.product_id) as product_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
GROUP BY u.user_id, u.name;
-- 查询购买了特定商品的用户
SELECT DISTINCT u.name
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
WHERE p.product_name = 'iPhone 15';
-- 查询从未下单的用户
SELECT u.user_id, u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
性能优化建议:
- JOIN顺序:小表在前,大表在后
- 索引优化:关联字段建立索引
- 避免笛卡尔积:确保有JOIN条件
- 提前过滤:WHERE条件尽早使用
-- 不好的写法(先JOIN再过滤)
SELECT *
FROM large_table1 t1
INNER JOIN large_table2 t2 ON t1.id = t2.id
WHERE t1.date >= '2024-01-01';
-- 好的写法(先过滤再JOIN)
SELECT *
FROM (
SELECT * FROM large_table1
WHERE date >= '2024-01-01'
) t1
INNER JOIN large_table2 t2 ON t1.id = t2.id;
SQL进阶技巧
▶ 如何使用窗口函数计算移动平均?
窗口函数可以在不分组的情况下进行聚合计算,非常适合时间序列分析。
场景:销售表sales(date, revenue),计算每日收入的7天移动平均。
-- 7天移动平均
SELECT
date,
revenue,
AVG(revenue) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) as ma_7d
FROM sales;
-- 包含更多统计指标
SELECT
date,
revenue,
-- 7天移动平均
ROUND(AVG(revenue) OVER (
ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2) as ma_7d,
-- 累计收入
SUM(revenue) OVER (
ORDER BY date
) as cumulative_revenue,
-- 环比增长率
ROUND(
(revenue - LAG(revenue, 1) OVER (ORDER BY date)) /
LAG(revenue, 1) OVER (ORDER BY date) * 100,
2
) as day_over_day_growth_pct,
-- 同比(7天前)
ROUND(
(revenue - LAG(revenue, 7) OVER (ORDER BY date)) /
LAG(revenue, 7) OVER (ORDER BY date) * 100,
2
) as week_over_week_growth_pct
FROM sales
ORDER BY date;
窗口函数框架(Frame)说明:
-- ROWS vs RANGE
-- ROWS: 按行数计算
AVG(revenue) OVER (ORDER BY date ROWS BETWEEN 2 PRECEDING AND CURRENT ROW)
-- 当前行和前2行,共3行
-- RANGE: 按值范围计算(日期)
AVG(revenue) OVER (
ORDER BY date
RANGE BETWEEN INTERVAL 7 DAY PRECEDING AND CURRENT ROW
)
-- 当前日期往前7天范围内的所有行
-- 常用框架
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 从开始到当前
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW -- 前6行到当前(7天窗口)
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -- 当前到结束
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING -- 前1行+当前+后1行
实战案例:股票技术指标
-- 计算股票的移动平均线和MACD
WITH price_data AS (
SELECT
date,
close_price,
-- MA5: 5日均线
AVG(close_price) OVER (
ORDER BY date
ROWS BETWEEN 4 PRECEDING AND CURRENT ROW
) as ma5,
-- MA10: 10日均线
AVG(close_price) OVER (
ORDER BY date
ROWS BETWEEN 9 PRECEDING AND CURRENT ROW
) as ma10,
-- MA20: 20日均线
AVG(close_price) OVER (
ORDER BY date
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) as ma20
FROM stock_prices
WHERE stock_code = '000001'
)
SELECT
date,
close_price,
ROUND(ma5, 2) as ma5,
ROUND(ma10, 2) as ma10,
ROUND(ma20, 2) as ma20,
-- 金叉/死叉信号
CASE
WHEN ma5 > ma10 AND LAG(ma5) OVER (ORDER BY date) <= LAG(ma10) OVER (ORDER BY date)
THEN '金叉'
WHEN ma5 < ma10 AND LAG(ma5) OVER (ORDER BY date) >= LAG(ma10) OVER (ORDER BY date)
THEN '死叉'
END as signal
FROM price_data
ORDER BY date DESC
LIMIT 30;
▶ 如何优化慢查询?
SQL性能优化是数据分析师必备技能,涉及索引、查询改写、表设计等多个方面。
场景:有一个慢查询需要优化。
优化步骤:
1. 使用EXPLAIN分析执行计划
EXPLAIN SELECT *
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE o.order_date >= '2024-01-01'
AND u.city = 'Shanghai';
关注指标:
type: 访问类型(ALL=全表扫描最差,const/eq_ref最好)rows: 扫描行数(越少越好)Extra: 额外信息(Using filesort/Using temporary需要优化)
2. 添加合适的索引
-- 问题:WHERE条件没有索引,导致全表扫描
-- 慢查询
SELECT * FROM orders
WHERE order_date >= '2024-01-01'
AND status = 'completed';
-- 解决:创建复合索引
CREATE INDEX idx_orders_date_status
ON orders(order_date, status);
-- 优化后查询
-- 现在可以使用索引快速定位
索引使用原则:
- WHERE、JOIN、ORDER BY的列考虑加索引
- 复合索引遵循最左前缀原则
- 区分度高的列放在前面
- 不要过度索引(影响写入性能)
**3. 避免SELECT ***
-- 慢查询
SELECT * FROM orders WHERE user_id = 123;
-- 优化:只查询需要的列
SELECT order_id, order_date, amount
FROM orders
WHERE user_id = 123;
减少数据传输量,提升查询速度。
4. 优化JOIN
-- 慢查询:先JOIN大表,再过滤
SELECT u.name, o.amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id
WHERE o.order_date >= '2024-01-01';
-- 优化:先过滤,再JOIN
SELECT u.name, o.amount
FROM users u
INNER JOIN (
SELECT user_id, amount
FROM orders
WHERE order_date >= '2024-01-01'
) o ON u.user_id = o.user_id;
5. 使用EXISTS代替IN(大数据集)
-- 慢查询:IN子查询
SELECT * FROM users
WHERE user_id IN (
SELECT user_id FROM orders WHERE amount > 1000
);
-- 优化:使用EXISTS
SELECT * FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.user_id AND o.amount > 1000
);
EXISTS在找到第一个匹配后就停止,IN需要完整结果集。
6. 避免函数操作索引列
-- 慢查询:函数导致索引失效
SELECT * FROM orders
WHERE YEAR(order_date) = 2024;
-- 优化:直接比较
SELECT * FROM orders
WHERE order_date >= '2024-01-01'
AND order_date < '2025-01-01';
7. 分页优化(深度分页)
-- 慢查询:深度分页
SELECT * FROM orders
ORDER BY order_id
LIMIT 100000, 20; -- 需要扫描100020行
-- 优化:使用WHERE代替OFFSET
SELECT * FROM orders
WHERE order_id > 100000 -- 上次查询的最后一个ID
ORDER BY order_id
LIMIT 20;
8. 批量操作代替循环
-- 慢:循环插入
-- for each row: INSERT INTO table VALUES (...)
-- 快:批量插入
INSERT INTO table (col1, col2, col3) VALUES
(val1, val2, val3),
(val4, val5, val6),
(val7, val8, val9);
-- 一次插入多行
性能优化检查清单:
✅ 索引优化
- WHERE、JOIN、ORDER BY字段有索引
- 复合索引顺序合理
- 定期清理无用索引
✅ 查询改写
- 避免SELECT *
- 子查询改JOIN
- 提前过滤减少数据量
✅ 表设计
- 合理的数据类型
- 避免过度范式化
- 考虑分区表
✅ 配置优化
- 增加缓存大小
- 调整连接数
- 启用查询缓存
▶ 如何处理NULL值?
NULL处理是SQL中的常见陷阱,需要特别注意。
NULL的特性:
- NULL表示"未知"或"不存在"
- NULL参与的任何运算结果都是NULL
- NULL不等于任何值,包括NULL本身
常见问题:
1. 比较陷阱
-- 错误:无法用 = 比较NULL
SELECT * FROM users WHERE age = NULL; -- 返回空(错误)
SELECT * FROM users WHERE age != NULL; -- 返回空(错误)
-- 正确:使用IS NULL / IS NOT NULL
SELECT * FROM users WHERE age IS NULL;
SELECT * FROM users WHERE age IS NOT NULL;
-- 判断两个可能为NULL的列是否相等
SELECT * FROM users
WHERE (col1 = col2) OR (col1 IS NULL AND col2 IS NULL);
2. 聚合函数与NULL
-- COUNT(*) vs COUNT(column)
SELECT
COUNT(*) as total_rows, -- 10行
COUNT(age) as age_count, -- 8行(忽略NULL)
COUNT(DISTINCT age) as unique_ages -- 7个(去重且忽略NULL)
FROM users;
-- SUM、AVG忽略NULL
SELECT
SUM(amount) as total, -- NULL不参与计算
AVG(amount) as average -- NULL不计入分母
FROM orders;
-- 区别:计算平均值时的NULL影响
SELECT
AVG(score) as avg1, -- 假设= 85(忽略NULL)
SUM(score) / COUNT(*) as avg2 -- 包含NULL行,可能= 75
FROM students;
3. NULL值替换
-- COALESCE:返回第一个非NULL值
SELECT
name,
COALESCE(phone, email, '无联系方式') as contact
FROM users;
-- IFNULL / ISNULL(MySQL)
SELECT
name,
IFNULL(age, 0) as age,
IFNULL(salary, 0) as salary
FROM employees;
-- CASE WHEN
SELECT
name,
CASE
WHEN age IS NULL THEN '未填写'
WHEN age < 18 THEN '未成年'
WHEN age >= 18 AND age < 60 THEN '成年'
ELSE '老年'
END as age_group
FROM users;
4. JOIN中的NULL
-- 左连接产生NULL
SELECT u.name, o.order_id
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;
-- 没有订单的用户,order_id为NULL
-- 过滤没有订单的用户
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
5. ORDER BY中的NULL
-- 默认行为(不同数据库可能不同)
SELECT * FROM users ORDER BY age; -- NULL在最前或最后
-- 控制NULL位置(MySQL)
SELECT * FROM users ORDER BY age IS NULL, age; -- NULL在最后
SELECT * FROM users ORDER BY age IS NOT NULL, age; -- NULL在最前
-- PostgreSQL
SELECT * FROM users ORDER BY age NULLS FIRST; -- NULL在前
SELECT * FROM users ORDER BY age NULLS LAST; -- NULL在后
6. 实战案例:用户活跃度分析
-- 计算用户最近活跃天数,未活跃显示为0
SELECT
user_id,
name,
COALESCE(
DATEDIFF(CURRENT_DATE, MAX(login_date)),
999
) as days_since_last_login,
CASE
WHEN MAX(login_date) IS NULL THEN '从未登录'
WHEN DATEDIFF(CURRENT_DATE, MAX(login_date)) <= 7 THEN '活跃'
WHEN DATEDIFF(CURRENT_DATE, MAX(login_date)) <= 30 THEN '一般'
ELSE '沉睡'
END as user_status
FROM users u
LEFT JOIN user_logins l ON u.user_id = l.user_id
GROUP BY u.user_id, u.name;
最佳实践:
数据库设计时明确NULL语义
- 能用NOT NULL就用NOT NULL
- NULL应该表示"未知"而非"空字符串"或"0"
查询时显式处理NULL
- 使用COALESCE提供默认值
- WHERE条件考虑NULL情况
测试边界情况
- 测试全部为NULL的列
- 测试NULL与非NULL混合
SQL实战场景
▶ 如何计算用户留存率?
留存率是衡量用户粘性的关键指标,SQL实现需要巧妙的日期处理。
场景:用户行为表user_actions(user_id, action_date),计算次日、7日、30日留存率。
留存率定义:
- 次日留存率 = 第N天注册的用户中,第N+1天还活跃的用户数 / 第N天注册的用户数
- 7日留存率 = 第N天注册的用户中,第N+7天还活跃的用户数 / 第N天注册的用户数
完整实现:
-- Step 1: 找出每个用户的首次行为日期(注册日期)
WITH first_action AS (
SELECT
user_id,
MIN(action_date) as first_date
FROM user_actions
GROUP BY user_id
),
-- Step 2: 关联用户的所有行为
user_retention AS (
SELECT
f.first_date,
f.user_id,
a.action_date,
DATEDIFF(a.action_date, f.first_date) as day_diff
FROM first_action f
INNER JOIN user_actions a ON f.user_id = a.user_id
),
-- Step 3: 计算留存
retention_stats AS (
SELECT
first_date,
COUNT(DISTINCT user_id) as new_users,
COUNT(DISTINCT CASE WHEN day_diff = 1 THEN user_id END) as day1_retained,
COUNT(DISTINCT CASE WHEN day_diff = 7 THEN user_id END) as day7_retained,
COUNT(DISTINCT CASE WHEN day_diff = 30 THEN user_id END) as day30_retained
FROM user_retention
GROUP BY first_date
)
-- Step 4: 计算留存率
SELECT
first_date,
new_users,
day1_retained,
ROUND(day1_retained * 100.0 / new_users, 2) as day1_retention_rate,
day7_retained,
ROUND(day7_retained * 100.0 / new_users, 2) as day7_retention_rate,
day30_retained,
ROUND(day30_retained * 100.0 / new_users, 2) as day30_retention_rate
FROM retention_stats
WHERE new_users >= 10 -- 过滤样本量过小的日期
ORDER BY first_date DESC;
方法2:使用自连接
-- 计算次日留存
SELECT
d1.first_date as cohort_date,
COUNT(DISTINCT d1.user_id) as new_users,
COUNT(DISTINCT d2.user_id) as retained_users,
ROUND(COUNT(DISTINCT d2.user_id) * 100.0 / COUNT(DISTINCT d1.user_id), 2) as retention_rate
FROM (
SELECT user_id, MIN(action_date) as first_date
FROM user_actions
GROUP BY user_id
) d1
LEFT JOIN user_actions d2
ON d1.user_id = d2.user_id
AND d2.action_date = DATE_ADD(d1.first_date, INTERVAL 1 DAY)
GROUP BY d1.first_date
ORDER BY d1.first_date DESC;
留存矩阵(Cohort Analysis):
-- 生成留存矩阵:每个注册日期的用户在后续每天的留存情况
WITH cohorts AS (
SELECT
user_id,
DATE(MIN(action_date)) as cohort_date
FROM user_actions
GROUP BY user_id
),
user_activities AS (
SELECT
c.cohort_date,
c.user_id,
DATE(a.action_date) as activity_date,
DATEDIFF(a.action_date, c.cohort_date) as days_since_cohort
FROM cohorts c
INNER JOIN user_actions a ON c.user_id = a.user_id
)
SELECT
cohort_date,
COUNT(DISTINCT CASE WHEN days_since_cohort = 0 THEN user_id END) as day0,
COUNT(DISTINCT CASE WHEN days_since_cohort = 1 THEN user_id END) as day1,
COUNT(DISTINCT CASE WHEN days_since_cohort = 2 THEN user_id END) as day2,
COUNT(DISTINCT CASE WHEN days_since_cohort = 3 THEN user_id END) as day3,
COUNT(DISTINCT CASE WHEN days_since_cohort = 7 THEN user_id END) as day7,
COUNT(DISTINCT CASE WHEN days_since_cohort = 14 THEN user_id END) as day14,
COUNT(DISTINCT CASE WHEN days_since_cohort = 30 THEN user_id END) as day30,
-- 计算留存率
ROUND(COUNT(DISTINCT CASE WHEN days_since_cohort = 1 THEN user_id END) * 100.0 /
COUNT(DISTINCT CASE WHEN days_since_cohort = 0 THEN user_id END), 2) as day1_rate,
ROUND(COUNT(DISTINCT CASE WHEN days_since_cohort = 7 THEN user_id END) * 100.0 /
COUNT(DISTINCT CASE WHEN days_since_cohort = 0 THEN user_id END), 2) as day7_rate
FROM user_activities
GROUP BY cohort_date
ORDER BY cohort_date DESC;
输出示例:
cohort_date day0 day1 day1_rate day7 day7_rate day30
2024-01-01 1000 720 72.00% 580 58.00% 420
2024-01-02 1050 756 72.00% 609 58.00% 441
2024-01-03 980 706 72.04% 568 57.96% 412
▶ 如何实现复杂的数据透视?
数据透视是将行数据转换为列数据,常用于报表展示。
场景:销售表sales(date, product, region, amount),需要按地区和产品展示销售额。
方法1:CASE WHEN实现
-- 将产品类别从行转为列
SELECT
region,
SUM(CASE WHEN product = 'iPhone' THEN amount ELSE 0 END) as iPhone_sales,
SUM(CASE WHEN product = 'iPad' THEN amount ELSE 0 END) as iPad_sales,
SUM(CASE WHEN product = 'MacBook' THEN amount ELSE 0 END) as MacBook_sales,
SUM(amount) as total_sales
FROM sales
WHERE date >= '2024-01-01'
GROUP BY region;
方法2:动态列(MySQL 8.0+)
-- 使用JSON聚合
SELECT
region,
JSON_OBJECTAGG(product, amount) as sales_by_product
FROM (
SELECT region, product, SUM(amount) as amount
FROM sales
GROUP BY region, product
) t
GROUP BY region;
方法3:行列互换(完整透视)
-- 透视示例:按月份和产品展示销售额
WITH monthly_sales AS (
SELECT
DATE_FORMAT(date, '%Y-%m') as month,
product,
SUM(amount) as amount
FROM sales
WHERE date >= '2024-01-01'
GROUP BY DATE_FORMAT(date, '%Y-%m'), product
)
SELECT
month,
SUM(CASE WHEN product = 'iPhone' THEN amount END) as iPhone,
SUM(CASE WHEN product = 'iPad' THEN amount END) as iPad,
SUM(CASE WHEN product = 'MacBook' THEN amount END) as MacBook,
SUM(CASE WHEN product = 'AirPods' THEN amount END) as AirPods,
SUM(amount) as total
FROM monthly_sales
GROUP BY month
ORDER BY month;
逆透视(列转行):
-- 将多列数据转换为行
-- 原表:region, q1_sales, q2_sales, q3_sales, q4_sales
-- 目标:region, quarter, sales
SELECT region, 'Q1' as quarter, q1_sales as sales FROM quarterly_sales
UNION ALL
SELECT region, 'Q2' as quarter, q2_sales as sales FROM quarterly_sales
UNION ALL
SELECT region, 'Q3' as quarter, q3_sales as sales FROM quarterly_sales
UNION ALL
SELECT region, 'Q4' as quarter, q4_sales as sales FROM quarterly_sales
ORDER BY region, quarter;
动态透视表生成:
-- 生成动态SQL进行透视(需要存储过程)
SET @sql = NULL;
SELECT
GROUP_CONCAT(
DISTINCT CONCAT(
'SUM(CASE WHEN product = ''',
product,
''' THEN amount ELSE 0 END) AS ',
REPLACE(product, ' ', '_')
)
) INTO @sql
FROM sales;
SET @sql = CONCAT('SELECT region, ', @sql, ', SUM(amount) as total
FROM sales
GROUP BY region');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
实战:构建销售仪表板数据
-- 多维度透视:按地区、月份、产品类别
SELECT
region,
DATE_FORMAT(date, '%Y-%m') as month,
-- 按产品类别
SUM(CASE WHEN category = 'Electronics' THEN amount END) as electronics,
SUM(CASE WHEN category = 'Clothing' THEN amount END) as clothing,
SUM(CASE WHEN category = 'Food' THEN amount END) as food,
-- 总计
SUM(amount) as total,
-- 环比增长
ROUND(
(SUM(amount) - LAG(SUM(amount)) OVER (PARTITION BY region ORDER BY DATE_FORMAT(date, '%Y-%m')))
/ LAG(SUM(amount)) OVER (PARTITION BY region ORDER BY DATE_FORMAT(date, '%Y-%m')) * 100,
2
) as mom_growth_pct
FROM sales
GROUP BY region, DATE_FORMAT(date, '%Y-%m')
ORDER BY region, month;
▶ 如何处理重复数据?
数据去重是数据清洗的常见需求,不同场景有不同的处理策略。
场景:订单表orders(order_id, user_id, product_id, amount, order_date)存在重复记录。
识别重复数据:
-- 1. 找出完全重复的记录
SELECT
user_id, product_id, amount, order_date,
COUNT(*) as duplicate_count
FROM orders
GROUP BY user_id, product_id, amount, order_date
HAVING COUNT(*) > 1;
-- 2. 找出部分字段重复(同一用户同一天多次下单同一商品)
SELECT
user_id, product_id, order_date,
COUNT(*) as order_count,
GROUP_CONCAT(order_id) as order_ids
FROM orders
GROUP BY user_id, product_id, order_date
HAVING COUNT(*) > 1;
-- 3. 统计重复率
SELECT
COUNT(*) as total_records,
COUNT(DISTINCT user_id, product_id, order_date) as unique_records,
COUNT(*) - COUNT(DISTINCT user_id, product_id, order_date) as duplicate_records,
ROUND((COUNT(*) - COUNT(DISTINCT user_id, product_id, order_date)) * 100.0 / COUNT(*), 2) as duplicate_rate
FROM orders;
删除重复数据:
-- 方法1:保留最小ID的记录
DELETE FROM orders
WHERE order_id NOT IN (
SELECT MIN(order_id)
FROM orders
GROUP BY user_id, product_id, order_date
);
-- 方法2:使用窗口函数(推荐,MySQL 8.0+)
DELETE FROM orders
WHERE order_id IN (
SELECT order_id FROM (
SELECT
order_id,
ROW_NUMBER() OVER (
PARTITION BY user_id, product_id, order_date
ORDER BY order_id
) as rn
FROM orders
) t
WHERE rn > 1
);
-- 方法3:创建新表(最安全)
CREATE TABLE orders_cleaned AS
SELECT * FROM (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY user_id, product_id, order_date
ORDER BY order_id
) as rn
FROM orders
) t
WHERE rn = 1;
-- 验证后替换原表
DROP TABLE orders;
ALTER TABLE orders_cleaned RENAME TO orders;
保留特定记录:
-- 场景:保留金额最大的订单
DELETE FROM orders
WHERE (user_id, product_id, order_date, amount) NOT IN (
SELECT user_id, product_id, order_date, MAX(amount)
FROM orders
GROUP BY user_id, product_id, order_date
);
-- 使用窗口函数保留最新的记录
WITH ranked_orders AS (
SELECT
*,
ROW_NUMBER() OVER (
PARTITION BY user_id, product_id
ORDER BY order_date DESC, order_id DESC
) as rn
FROM orders
)
DELETE FROM orders
WHERE order_id IN (
SELECT order_id FROM ranked_orders WHERE rn > 1
);
预防重复:
-- 1. 添加唯一索引
CREATE UNIQUE INDEX idx_unique_order
ON orders(user_id, product_id, order_date);
-- 2. 插入时检查
INSERT INTO orders (user_id, product_id, amount, order_date)
SELECT 123, 456, 100.00, '2024-01-01'
WHERE NOT EXISTS (
SELECT 1 FROM orders
WHERE user_id = 123
AND product_id = 456
AND order_date = '2024-01-01'
);
-- 3. 使用INSERT IGNORE(MySQL)
INSERT IGNORE INTO orders (user_id, product_id, amount, order_date)
VALUES (123, 456, 100.00, '2024-01-01');
-- 4. 使用ON DUPLICATE KEY UPDATE
INSERT INTO orders (user_id, product_id, amount, order_date)
VALUES (123, 456, 100.00, '2024-01-01')
ON DUPLICATE KEY UPDATE amount = VALUES(amount);
大表去重优化:
-- 分批删除,避免锁表时间过长
SET @batch_size = 10000;
SET @deleted = 1;
WHILE @deleted > 0 DO
DELETE FROM orders
WHERE order_id IN (
SELECT order_id FROM (
SELECT order_id FROM (
SELECT
order_id,
ROW_NUMBER() OVER (
PARTITION BY user_id, product_id, order_date
ORDER BY order_id
) as rn
FROM orders
) t
WHERE rn > 1
LIMIT @batch_size
) t2
);
SET @deleted = ROW_COUNT();
SELECT CONCAT('Deleted ', @deleted, ' rows') as status;
-- 暂停,让其他事务有机会执行
DO SLEEP(1);
END WHILE;
▶ 如何实现行列转换(PIVOT/UNPIVOT)?
行列转换是报表开发中的常见需求,可以改变数据的展现形式。
场景:学生成绩表scores(student, subject, score),需要将科目从行转为列。
列转行(CASE WHEN实现):
-- 原始数据
student subject score
张三 语文 85
张三 数学 90
张三 英语 88
李四 语文 92
李四 数学 87
李四 英语 91
-- 目标:每个学生一行,科目作为列
SELECT
student,
MAX(CASE WHEN subject = '语文' THEN score END) as 语文,
MAX(CASE WHEN subject = '数学' THEN score END) as 数学,
MAX(CASE WHEN subject = '英语' THEN score END) as 英语,
ROUND(AVG(score), 2) as 平均分
FROM scores
GROUP BY student;
-- 结果
student 语文 数学 英语 平均分
张三 85 90 88 87.67
李四 92 87 91 90.00
使用PIVOT(SQL Server/Oracle):
-- SQL Server语法
SELECT *
FROM (
SELECT student, subject, score
FROM scores
) AS source_table
PIVOT (
MAX(score)
FOR subject IN ([语文], [数学], [英语])
) AS pivot_table;
-- Oracle语法
SELECT *
FROM scores
PIVOT (
MAX(score)
FOR subject IN ('语文' AS 语文, '数学' AS 数学, '英语' AS 英语)
);
行转列(UNPIVOT):
-- 原始数据(宽表)
student 语文 数学 英语
张三 85 90 88
李四 92 87 91
-- 目标:转换为长表
-- MySQL实现(UNION ALL)
SELECT student, '语文' as subject, 语文 as score FROM student_scores
UNION ALL
SELECT student, '数学' as subject, 数学 as score FROM student_scores
UNION ALL
SELECT student, '英语' as subject, 英语 as score FROM student_scores
ORDER BY student, subject;
-- SQL Server UNPIVOT
SELECT student, subject, score
FROM student_scores
UNPIVOT (
score FOR subject IN (语文, 数学, 英语)
) AS unpivot_table;
动态PIVOT(MySQL):
-- 动态生成科目列
SET @sql = NULL;
SELECT GROUP_CONCAT(
DISTINCT CONCAT(
'MAX(CASE WHEN subject = ''', subject, ''' THEN score END) AS `', subject, '`'
)
) INTO @sql
FROM scores;
SET @sql = CONCAT('SELECT student, ', @sql, ', ROUND(AVG(score), 2) as 平均分
FROM scores
GROUP BY student');
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
▶ 如何实现树形结构查询(递归CTE)?
树形结构常用于组织架构、商品分类、评论回复等场景。
场景:员工表employees(id, name, manager_id),查询组织架构。
向下递归(查找所有下属):
-- MySQL 8.0+ / PostgreSQL / SQL Server
WITH RECURSIVE employee_tree AS (
-- 锚点:起始节点
SELECT id, name, manager_id, 1 as level, name as path
FROM employees
WHERE id = 1 -- CEO
UNION ALL
-- 递归部分:查找下属
SELECT e.id, e.name, e.manager_id, et.level + 1,
CONCAT(et.path, ' > ', e.name) as path
FROM employees e
INNER JOIN employee_tree et ON e.manager_id = et.id
WHERE et.level < 10 -- 防止无限递归
)
SELECT id, name, manager_id, level, path
FROM employee_tree
ORDER BY level, id;
-- 结果示例
id name manager_id level path
1 张总 NULL 1 张总
2 李经理 1 2 张总 > 李经理
3 王经理 1 2 张总 > 王经理
4 赵主管 2 3 张总 > 李经理 > 赵主管
5 钱主管 2 3 张总 > 李经理 > 钱主管
向上递归(查找所有上级):
WITH RECURSIVE manager_chain AS (
-- 锚点:当前员工
SELECT id, name, manager_id, 1 as level
FROM employees
WHERE id = 5 -- 查找ID=5的员工的所有上级
UNION ALL
-- 递归:查找上级
SELECT e.id, e.name, e.manager_id, mc.level + 1
FROM employees e
INNER JOIN manager_chain mc ON e.id = mc.manager_id
)
SELECT * FROM manager_chain
ORDER BY level DESC; -- 从上到下展示
商品分类树:
-- 表结构:categories(id, name, parent_id)
-- 查找某分类下的所有子分类
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, 1 as level,
CAST(id AS CHAR(200)) as path
FROM categories
WHERE id = 10 -- 起始分类
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.level + 1,
CONCAT(ct.path, ',', c.id) as path
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree
ORDER BY path;
▶ 如何实现分页查询?
分页是Web应用的基础功能,需要考虑性能和准确性。
基础分页(LIMIT + OFFSET):
-- MySQL/PostgreSQL
SELECT *
FROM products
ORDER BY product_id
LIMIT 20 OFFSET 40; -- 第3页,每页20条
-- SQL Server
SELECT *
FROM products
ORDER BY product_id
OFFSET 40 ROWS
FETCH NEXT 20 ROWS ONLY;
深度分页优化(游标分页):
-- 问题:LIMIT 100000, 20 需要扫描100020行
-- 慢查询示例
SELECT * FROM orders
ORDER BY order_id
LIMIT 100000, 20;
-- 优化方案1:使用WHERE代替OFFSET
SELECT * FROM orders
WHERE order_id > 100000 -- 上一页最后一个ID
ORDER BY order_id
LIMIT 20;
-- 优化方案2:子查询只查ID
SELECT o.*
FROM orders o
INNER JOIN (
SELECT order_id
FROM orders
ORDER BY order_id
LIMIT 20 OFFSET 100000
) t ON o.order_id = t.order_id;
游标分页(推荐用于API):
-- 基于游标的分页(无OFFSET性能问题)
-- 首页
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
ORDER BY created_at DESC, order_id DESC
LIMIT 20;
-- 下一页(传入上一页最后记录的游标)
SELECT * FROM orders
WHERE created_at >= '2024-01-01'
AND (created_at < '2024-01-15 10:00:00'
OR (created_at = '2024-01-15 10:00:00' AND order_id < 12345))
ORDER BY created_at DESC, order_id DESC
LIMIT 20;
▶ 如何进行日期时间处理与计算?
日期时间处理是数据分析中的高频需求,掌握常用函数和场景至关重要。
基础日期函数(MySQL):
-- 当前日期时间
SELECT NOW(); -- 2024-01-15 14:30:25
SELECT CURDATE(); -- 2024-01-15
SELECT CURTIME(); -- 14:30:25
-- 提取日期部分
SELECT DATE('2024-01-15 14:30:25'); -- 2024-01-15
SELECT YEAR('2024-01-15'); -- 2024
SELECT MONTH('2024-01-15'); -- 1
SELECT DAY('2024-01-15'); -- 15
SELECT HOUR('2024-01-15 14:30:25'); -- 14
SELECT MINUTE('2024-01-15 14:30:25'); -- 30
SELECT SECOND('2024-01-15 14:30:25'); -- 25
-- 星期处理
SELECT DAYOFWEEK('2024-01-15'); -- 2 (1=周日, 7=周六)
SELECT WEEKDAY('2024-01-15'); -- 0 (0=周一, 6=周日)
SELECT DAYNAME('2024-01-15'); -- Monday
SELECT WEEK('2024-01-15'); -- 3 (第几周)
日期格式化:
-- DATE_FORMAT 格式化日期
SELECT DATE_FORMAT('2024-01-15 14:30:25', '%Y-%m-%d'); -- 2024-01-15
SELECT DATE_FORMAT('2024-01-15 14:30:25', '%Y年%m月%d日'); -- 2024年01月15日
SELECT DATE_FORMAT('2024-01-15 14:30:25', '%Y-%m-%d %H:%i:%s'); -- 2024-01-15 14:30:25
SELECT DATE_FORMAT('2024-01-15 14:30:25', '%W, %M %d, %Y'); -- Monday, January 15, 2024
-- 常用格式符号:
-- %Y: 4位年份 %y: 2位年份
-- %m: 2位月份 %c: 1位月份
-- %d: 2位日期 %e: 1位日期
-- %H: 24小时 %h: 12小时
-- %i: 分钟 %s: 秒
-- %W: 星期名 %a: 星期缩写
日期计算:
日期加减运算:
-- DATE_ADD / DATE_SUB
SELECT DATE_ADD('2024-01-15', INTERVAL 1 DAY); -- 2024-01-16
SELECT DATE_ADD('2024-01-15', INTERVAL 7 DAY); -- 2024-01-22
SELECT DATE_ADD('2024-01-15', INTERVAL 1 MONTH); -- 2024-02-15
SELECT DATE_ADD('2024-01-15', INTERVAL 1 YEAR); -- 2025-01-15
SELECT DATE_ADD('2024-01-15', INTERVAL -1 MONTH); -- 2023-12-15
SELECT DATE_SUB('2024-01-15', INTERVAL 7 DAY); -- 2024-01-08
-- 组合单位
SELECT DATE_ADD('2024-01-15 14:30:00', INTERVAL '1 2:30:00' DAY_SECOND);
-- 1天2小时30分钟后
-- 加号运算符(简写)
SELECT '2024-01-15' + INTERVAL 1 DAY;
SELECT NOW() - INTERVAL 7 DAY; -- 7天前
时间差计算:
-- DATEDIFF:计算天数差
SELECT DATEDIFF('2024-01-20', '2024-01-15'); -- 5
-- TIMESTAMPDIFF:计算任意单位差值
SELECT TIMESTAMPDIFF(SECOND, '2024-01-15 10:00:00', '2024-01-15 11:30:00'); -- 5400
SELECT TIMESTAMPDIFF(MINUTE, '2024-01-15 10:00:00', '2024-01-15 11:30:00'); -- 90
SELECT TIMESTAMPDIFF(HOUR, '2024-01-15 10:00:00', '2024-01-15 14:00:00'); -- 4
SELECT TIMESTAMPDIFF(DAY, '2024-01-01', '2024-01-31'); -- 30
SELECT TIMESTAMPDIFF(MONTH, '2024-01-01', '2024-06-01'); -- 5
SELECT TIMESTAMPDIFF(YEAR, '2020-01-01', '2024-01-01'); -- 4
-- 时间差(秒数)
SELECT UNIX_TIMESTAMP('2024-01-15 14:00:00') - UNIX_TIMESTAMP('2024-01-15 10:00:00'); -- 14400
月末/月初处理:
-- 获取月初
SELECT DATE_FORMAT('2024-01-15', '%Y-%m-01'); -- 2024-01-01
-- 获取月末
SELECT LAST_DAY('2024-01-15'); -- 2024-01-31
SELECT LAST_DAY('2024-02-15'); -- 2024-02-29 (闰年)
-- 下个月初
SELECT DATE_ADD(DATE_FORMAT('2024-01-15', '%Y-%m-01'), INTERVAL 1 MONTH); -- 2024-02-01
-- 上个月末
SELECT LAST_DAY(DATE_SUB('2024-01-15', INTERVAL 1 MONTH)); -- 2023-12-31
-- 季度处理
SELECT QUARTER('2024-01-15'); -- 1
SELECT CONCAT('Q', QUARTER('2024-01-15'), '-', YEAR('2024-01-15')); -- Q1-2024
-- 季度初
SELECT DATE_FORMAT(
DATE_SUB('2024-04-15', INTERVAL (MONTH('2024-04-15') - 1) % 3 MONTH),
'%Y-%m-01'
); -- 2024-04-01
工作日计算(需自定义逻辑):
-- 判断是否工作日(排除周末)
SELECT
order_date,
CASE
WHEN WEEKDAY(order_date) IN (5, 6) THEN '周末'
ELSE '工作日'
END as day_type
FROM orders;
-- 计算两日期间的工作日数量(不含节假日表)
WITH RECURSIVE date_series AS (
SELECT '2024-01-01' AS date
UNION ALL
SELECT DATE_ADD(date, INTERVAL 1 DAY)
FROM date_series
WHERE date < '2024-01-31'
)
SELECT COUNT(*) as workdays
FROM date_series
WHERE WEEKDAY(date) NOT IN (5, 6); -- 排除周六周日
-- 添加N个工作日后的日期(简化版,不考虑节假日)
-- 实际场景需配合节假日表
实战场景:
场景1:统计最近30天每日订单数:
SELECT
DATE(order_time) as order_date,
COUNT(*) as order_count,
SUM(amount) as total_amount
FROM orders
WHERE order_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY DATE(order_time)
ORDER BY order_date;
场景2:计算用户注册后首单时长:
SELECT
u.user_id,
u.register_time,
MIN(o.order_time) as first_order_time,
TIMESTAMPDIFF(HOUR, u.register_time, MIN(o.order_time)) as hours_to_first_order,
DATEDIFF(MIN(o.order_time), u.register_time) as days_to_first_order
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.register_time;
场景3:按月统计累计用户数:
SELECT
DATE_FORMAT(register_time, '%Y-%m') as month,
COUNT(*) as new_users,
SUM(COUNT(*)) OVER (ORDER BY DATE_FORMAT(register_time, '%Y-%m')) as cumulative_users
FROM users
GROUP BY DATE_FORMAT(register_time, '%Y-%m')
ORDER BY month;
场景4:计算用户年龄(从身份证或生日):
SELECT
user_id,
birthday,
TIMESTAMPDIFF(YEAR, birthday, CURDATE()) as age,
-- 精确年龄(考虑当年生日是否已过)
YEAR(CURDATE()) - YEAR(birthday) -
(DATE_FORMAT(CURDATE(), '%m%d') < DATE_FORMAT(birthday, '%m%d')) as exact_age
FROM users;
场景5:查找连续登录天数:
WITH login_dates AS (
SELECT DISTINCT
user_id,
DATE(login_time) as login_date
FROM login_logs
),
date_groups AS (
SELECT
user_id,
login_date,
DATE_SUB(login_date, INTERVAL ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date) DAY) as group_date
FROM login_dates
)
SELECT
user_id,
MIN(login_date) as start_date,
MAX(login_date) as end_date,
COUNT(*) as consecutive_days
FROM date_groups
GROUP BY user_id, group_date
HAVING COUNT(*) >= 3 -- 至少连续3天
ORDER BY consecutive_days DESC;
场景6:同比/环比计算:
-- 月度同比(去年同期)
SELECT
DATE_FORMAT(order_date, '%Y-%m') as month,
SUM(amount) as current_amount,
LAG(SUM(amount), 12) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) as last_year_amount,
ROUND((SUM(amount) - LAG(SUM(amount), 12) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')))
/ LAG(SUM(amount), 12) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) * 100, 2) as yoy_growth
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m');
-- 月度环比(上个月)
SELECT
DATE_FORMAT(order_date, '%Y-%m') as month,
SUM(amount) as current_amount,
LAG(SUM(amount), 1) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) as last_month_amount,
ROUND((SUM(amount) - LAG(SUM(amount), 1) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')))
/ LAG(SUM(amount), 1) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) * 100, 2) as mom_growth
FROM orders
GROUP BY DATE_FORMAT(order_date, '%Y-%m');
不同数据库差异:
| 功能 | MySQL | PostgreSQL | SQL Server |
|---|---|---|---|
| 当前时间 | NOW() | NOW() / CURRENT_TIMESTAMP | GETDATE() |
| 日期加减 | DATE_ADD() | date + INTERVAL '1 day' | DATEADD() |
| 日期差 | DATEDIFF() | date2 - date1 | DATEDIFF() |
| 格式化 | DATE_FORMAT() | TO_CHAR() | FORMAT() |
| 提取部分 | YEAR(), MONTH() | EXTRACT(YEAR FROM date) | YEAR(), MONTH() |
注意事项:
- 时区问题:注意服务器时区、数据库时区和应用时区的一致性
- 索引优化:日期范围查询时避免在WHERE中对日期列使用函数
-- 差❌ WHERE DATE(order_time) = '2024-01-15' -- 好✅ WHERE order_time >= '2024-01-15' AND order_time < '2024-01-16' - 闰年处理:2月29日、年度计算需考虑闰年
- 夏令时:跨夏令时的时间计算可能出现偏差
- 性能考虑:大数据量时间范围查询建议分区表
▶ SQL的SELECT语句执行顺序是怎样的?
很多人认为SQL语句是从SELECT开始执行的,但实际的执行顺序与书写顺序不同。
标准执行顺序:
(8) SELECT (9) DISTINCT (11) TOP
(1) FROM
(3) JOIN
(2) ON
(4) WHERE
(5) GROUP BY
(6) WITH CUBE 或 WITH ROLLUP
(7) HAVING
(10) ORDER BY
(12) LIMIT/OFFSET
详细说明:
1. FROM:首先确定数据来源
2. ON:对JOIN操作的表应用ON条件
3. JOIN:执行JOIN操作,合并表
4. WHERE:对合并后的结果集应用WHERE过滤条件
5. GROUP BY:按指定列分组
6. 聚合函数:计算每组的聚合值
7. HAVING:对分组后的结果进行过滤
8. SELECT:选择要返回的列
9. DISTINCT:去除重复行
10. ORDER BY:对结果排序
11. LIMIT/OFFSET:限制返回的行数
实际案例分析:
-- 查询每个城市总消费超过10000的用户数量
SELECT
city,
COUNT(DISTINCT user_id) as user_count,
SUM(amount) as total_amount
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE order_date >= '2024-01-01'
GROUP BY city
HAVING SUM(amount) > 10000
ORDER BY total_amount DESC
LIMIT 5;
执行过程:
- FROM orders o:扫描orders表
- JOIN users u:连接users表
- ON o.user_id = u.user_id:匹配连接条件
- WHERE order_date >= ‘2024-01-01’:过滤2024年的订单
- GROUP BY city:按城市分组
- SUM(amount):计算每个城市的总金额
- HAVING SUM(amount) > 10000:筛选总额超过10000的城市
- SELECT:选择要显示的列
- ORDER BY total_amount DESC:按总金额降序
- LIMIT 5:只返回前5条
优化建议:
基于执行顺序的优化策略:
- WHERE优先于HAVING:能在WHERE中过滤的就不要在HAVING中过滤
-- 慢❌(先分组再过滤)
GROUP BY user_id
HAVING user_id > 1000
-- 快✅(先过滤再分组)
WHERE user_id > 1000
GROUP BY user_id
- JOIN前先过滤:使用子查询先过滤再JOIN
-- 慢❌
FROM large_table t1
JOIN large_table t2 ON t1.id = t2.id
WHERE t1.date >= '2024-01-01'
-- 快✅
FROM (SELECT * FROM large_table WHERE date >= '2024-01-01') t1
JOIN large_table t2 ON t1.id = t2.id
- 避免在WHERE中使用函数:会导致索引失效
-- 慢❌
WHERE YEAR(order_date) = 2024
-- 快✅
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'
▶ 什么是索引?索引的类型和使用场景?
索引是数据库表中一列或多列值的数据结构,可以加快数据检索速度。
索引类型:
1. 主键索引(PRIMARY KEY)
- 唯一且非空
- 每个表只能有一个主键
- 自动创建聚簇索引(InnoDB)
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
);
2. 唯一索引(UNIQUE)
- 列值必须唯一,但可以有NULL
- 可以有多个唯一索引
-- 单列唯一索引
CREATE UNIQUE INDEX idx_email ON users(email);
-- 多列唯一索引
CREATE UNIQUE INDEX idx_username_email ON users(username, email);
3. 普通索引(INDEX)
- 最基本的索引,无唯一性限制
-- 单列索引
CREATE INDEX idx_name ON users(name);
-- 多列索引(复合索引)
CREATE INDEX idx_city_age ON users(city, age);
4. 全文索引(FULLTEXT)
- 用于全文搜索
- 只支持CHAR、VARCHAR、TEXT类型
CREATE FULLTEXT INDEX idx_content ON articles(title, content);
-- 使用
SELECT * FROM articles
WHERE MATCH(title, content) AGAINST('数据分析');
索引数据结构:
B+树索引(InnoDB默认):
- 所有数据存储在叶子节点
- 叶子节点形成有序链表
- 范围查询效率高
哈希索引(Memory引擎):
- 等值查询快
- 不支持范围查询
- 不支持排序
索引使用场景:
适合建索引:
- WHERE条件频繁的列
CREATE INDEX idx_user_id ON orders(user_id);
- JOIN连接的列
CREATE INDEX idx_user_id ON orders(user_id);
- ORDER BY排序的列
CREATE INDEX idx_order_date ON orders(order_date);
- GROUP BY分组的列
CREATE INDEX idx_category ON products(category);
不适合建索引:
- 数据量小的表(<1000行)
- 频繁更新的列(维护索引开销大)
- 区分度低的列(如性别,只有男/女)
- 长文本列(可以对前缀建索引)
复合索引的最左前缀原则:
-- 创建复合索引
CREATE INDEX idx_abc ON table(a, b, c);
-- 可以使用索引的查询
WHERE a = 1 ✅
WHERE a = 1 AND b = 2 ✅
WHERE a = 1 AND b = 2 AND c = 3 ✅
WHERE a = 1 AND c = 3 ✅ (只用到a)
-- 不能使用索引
WHERE b = 2 ❌
WHERE c = 3 ❌
WHERE b = 2 AND c = 3 ❌
索引优化建议:
-- 1. 覆盖索引:查询的列都在索引中
CREATE INDEX idx_user_name_age ON users(user_id, name, age);
SELECT name, age FROM users WHERE user_id = 1;
-- 不需要回表,直接从索引获取数据
-- 2. 前缀索引:对长字符串建立前缀索引
CREATE INDEX idx_email_prefix ON users(email(10));
-- 3. 查看索引使用情况
EXPLAIN SELECT * FROM users WHERE user_id = 1;
-- 4. 删除无用索引
SELECT * FROM sys.schema_unused_indexes;
▶ 什么是事务?ACID特性如何理解?
事务是数据库执行的一系列操作的集合,这些操作要么全部成功,要么全部失败。
ACID特性:
1. 原子性(Atomicity)
事务中的所有操作要么全部完成,要么全部不完成。
-- 转账场景:A转给B 100元
START TRANSACTION;
-- 操作1:A账户减100
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A';
-- 操作2:B账户加100
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B';
-- 如果任何一步失败,全部回滚
COMMIT; -- 全部成功
-- 或
ROLLBACK; -- 全部撤销
2. 一致性(Consistency)
事务执行前后,数据库从一个一致性状态转换到另一个一致性状态。
-- 约束保证一致性
ALTER TABLE accounts ADD CONSTRAINT chk_balance CHECK (balance >= 0);
START TRANSACTION;
-- 这个操作会违反约束,事务会失败
UPDATE accounts SET balance = balance - 1000 WHERE user_id = 'A';
-- 错误:balance不能为负数
ROLLBACK;
3. 隔离性(Isolation)
并发执行的事务之间互不干扰。
隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
脏读示例:
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = 1000 WHERE user_id = 'A';
-- 未提交
-- 事务B(READ UNCOMMITTED)
SELECT balance FROM accounts WHERE user_id = 'A'; -- 读到1000(脏数据)
-- 事务A回滚
ROLLBACK;
-- 事务B读到的数据是无效的
不可重复读示例:
-- 事务A
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 'A'; -- 第一次读:1000
-- 事务B修改并提交
UPDATE accounts SET balance = 2000 WHERE user_id = 'A';
COMMIT;
-- 事务A再次读取
SELECT balance FROM accounts WHERE user_id = 'A'; -- 第二次读:2000
-- 同一事务内,两次读取结果不一致
幻读示例:
-- 事务A
START TRANSACTION;
SELECT COUNT(*) FROM orders WHERE amount > 1000; -- 第一次读:5条
-- 事务B插入新数据
INSERT INTO orders VALUES (6, 1500);
COMMIT;
-- 事务A再次统计
SELECT COUNT(*) FROM orders WHERE amount > 1000; -- 第二次读:6条
-- 出现了"幻影"行
4. 持久性(Durability)
事务提交后,对数据的修改永久保存,即使系统崩溃也不会丢失。
START TRANSACTION;
INSERT INTO orders (user_id, amount) VALUES (123, 999);
COMMIT; -- 一旦提交,即使服务器立即崩溃,数据也不会丢失
实战案例:秒杀场景
-- 场景:商品秒杀,防止超卖
START TRANSACTION;
-- 1. 锁定库存行(for update)
SELECT stock FROM products
WHERE product_id = 123
FOR UPDATE;
-- 2. 检查库存
IF stock > 0 THEN
-- 3. 扣减库存
UPDATE products
SET stock = stock - 1
WHERE product_id = 123;
-- 4. 创建订单
INSERT INTO orders (user_id, product_id, amount)
VALUES (456, 123, 99);
COMMIT;
ELSE
ROLLBACK;
END IF;
性能优化建议:
- 事务尽可能短:减少锁定时间
- 避免在事务中查询大量数据
- 合理选择隔离级别:根据业务需求权衡性能和一致性
- 使用乐观锁替代悲观锁(适合读多写少场景)
-- 乐观锁示例(使用version字段)
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE product_id = 123
AND version = 10; -- 只有version匹配才更新
-- 如果更新失败(affected rows = 0),说明有其他事务修改了数据
▶ explain执行计划怎么看?如何分析SQL性能?
EXPLAIN是分析SQL查询性能的重要工具,可以显示MySQL如何执行查询。
基本用法:
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
重要字段详解:
| 字段 | 说明 | 重要性 |
|---|---|---|
| type | 访问类型 | ⭐⭐⭐⭐⭐ |
| key | 实际使用的索引 | ⭐⭐⭐⭐⭐ |
| rows | 扫描的行数 | ⭐⭐⭐⭐⭐ |
| Extra | 额外信息 | ⭐⭐⭐⭐⭐ |
type(访问类型)
性能从好到差:system > const > eq_ref > ref > range > index > ALL
-- const:最优,主键或唯一索引的常量查询
EXPLAIN SELECT * FROM users WHERE user_id = 1;
-- type: const
-- ref:非唯一索引扫描
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
-- type: ref
-- range:范围扫描
EXPLAIN SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31';
-- type: range
-- ALL:全表扫描(最差)
EXPLAIN SELECT * FROM orders WHERE YEAR(order_date) = 2024;
-- type: ALL(函数导致索引失效)
Extra(额外信息)
好的Extra:
Using index:覆盖索引,不需要回表Using where:使用WHERE过滤Using index condition:索引下推
坏的Extra:
Using filesort:需要额外排序Using temporary:需要临时表Using join buffer:JOIN时需要缓冲区
完整案例分析:
-- 慢查询
EXPLAIN SELECT
u.name,
COUNT(o.order_id) as order_count,
SUM(o.amount) as total_amount
FROM orders o
JOIN users u ON o.user_id = u.user_id
WHERE o.order_date >= '2024-01-01'
AND u.city = 'Beijing'
GROUP BY u.user_id
ORDER BY total_amount DESC
LIMIT 10;
可能的执行计划问题:
id select_type table type key rows Extra
1 SIMPLE o ALL NULL 100000 Using where; Using temporary; Using filesort
1 SIMPLE u eq_ref PRIMARY 1 Using where
问题分析:
type: ALL- orders表全表扫描key: NULL- 没有使用索引rows: 100000- 扫描10万行Using temporary- 使用临时表Using filesort- 需要排序
优化方案:
-- 1. 在order_date建立索引
CREATE INDEX idx_order_date ON orders(order_date);
-- 2. 在user_id建立索引
CREATE INDEX idx_user_id ON orders(user_id);
-- 3. 在city建立索引
CREATE INDEX idx_city ON users(city);
-- 4. 创建复合索引
CREATE INDEX idx_date_user ON orders(order_date, user_id);
优化后的执行计划:
id select_type table type key rows Extra
1 SIMPLE o range idx_date_user 5000 Using where; Using index
1 SIMPLE u eq_ref PRIMARY 1 Using where
改进:
type: range- 使用范围扫描key: idx_date_user- 使用了索引rows: 5000- 扫描行数减少95%- 去除了
Using temporary和Using filesort
性能分析检查清单:
✅ type 是否为 ALL 或 index(全表扫描) ✅ key 是否为 NULL(未使用索引) ✅ rows 是否过大 ✅ Extra 是否包含 Using filesort 或 Using temporary ✅ 多表JOIN时,驱动表选择是否合理
▶ 如何设计一个高效的数据库表结构?
数据库表设计直接影响系统性能和可维护性。
设计原则:
1. 三大范式
第一范式(1NF):字段不可再分
-- 不符合1NF❌
CREATE TABLE users (
address TEXT -- "北京市朝阳区XXX街道"(混合了省、市、区)
);
-- 符合1NF✅
CREATE TABLE users (
province VARCHAR(20),
city VARCHAR(20),
district VARCHAR(20),
street VARCHAR(100)
);
第二范式(2NF):消除部分依赖
-- 不符合2NF❌
CREATE TABLE order_items (
order_id INT,
product_id INT,
product_name VARCHAR(50), -- 只依赖product_id
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- 符合2NF✅
CREATE TABLE order_items (
order_id INT,
product_id INT,
quantity INT,
PRIMARY KEY (order_id, product_id)
);
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(50)
);
第三范式(3NF):消除传递依赖
-- 不符合3NF❌
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
dept_id INT,
dept_name VARCHAR(50), -- 依赖于dept_id
dept_location VARCHAR(100) -- 依赖于dept_id
);
-- 符合3NF✅
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
dept_id INT,
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50),
location VARCHAR(100)
);
2. 反范式化(性能优化)
在某些场景下,为了提高查询性能,可以适当违反范式。
-- 反范式化(冗余用户信息)
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
user_name VARCHAR(50), -- 冗余
shipping_address TEXT, -- 冗余(快照)
order_amount DECIMAL(10,2)
);
优势:
- 减少JOIN,查询更快
- 保存历史快照(地址可能变化)
劣势:
- 数据冗余
- 更新复杂
- 可能不一致
3. 数据类型选择
原则:选择最小的数据类型
CREATE TABLE optimized_users (
user_id INT UNSIGNED, -- 4字节
age TINYINT UNSIGNED, -- 1字节,0-255
username VARCHAR(50), -- 变长
gender CHAR(1), -- 定长
status ENUM('active', 'inactive'), -- 1字节
balance DECIMAL(10,2), -- 精确,适合金额
created_at DATETIME, -- 8字节
settings JSON -- 灵活的配置数据
);
4. 索引设计
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
order_date DATETIME NOT NULL,
amount DECIMAL(10,2),
status ENUM('pending','paid','shipped'),
-- 索引
INDEX idx_user_id (user_id),
INDEX idx_date_status (order_date, status),
UNIQUE INDEX idx_order_no (order_no)
);
5. 实战案例:用户表设计
CREATE TABLE users (
user_id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
status TINYINT UNSIGNED DEFAULT 1,
-- 统计信息(冗余字段,提高查询性能)
login_count INT UNSIGNED DEFAULT 0,
order_count INT UNSIGNED DEFAULT 0,
-- 时间戳
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME COMMENT '软删除',
-- 索引
UNIQUE INDEX idx_username (username),
UNIQUE INDEX idx_email (email),
INDEX idx_status (status),
INDEX idx_deleted (deleted_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
最佳实践:
✅ 所有表必须有主键 ✅ 使用InnoDB引擎(支持事务) ✅ 使用UTF8MB4字符集(支持emoji) ✅ 重要字段添加NOT NULL和DEFAULT ✅ 合理使用索引 ✅ 添加必要的注释 ✅ 考虑软删除而非物理删除 ✅ 预留扩展字段(JSON)
▶ MySQL 的 InnoDB 和 MyISAM 存储引擎有什么区别?
- InnoDB 支持事务和外键,MyISAM 不支持。
- InnoDB 支持行级锁,MyISAM 只支持表级锁。
- InnoDB 的数据和索引是存储在同一个文件中的,MyISAM 是分开存储的。
▶ MySQL 的索引有哪些类型?
▶ MySQL 的事务隔离级别有哪些?
▶ MySQL 的 B+ 树索引有什么特点?
- 所有数据都存储在叶子节点上。
- 叶子节点之间通过指针连接,形成一个有序链表。
- 非叶子节点只存储索引,不存储数据。
▶ MySQL 的 MVCC 是什么?
▶ MySQL 的 explain 命令有什么用?
▶ MySQL 的 binlog 有什么用?
▶ MySQL 的慢查询日志有什么用?
▶ MySQL 的分库分表有哪些方式?
- 垂直拆分:将一个大表按列拆分成多个小表。
- 水平拆分:将一个大表按行拆分成多个小表。
▶ MySQL 的索引优化有哪些方法?
- 避免在索引列上使用函数或表达式。
- 使用覆盖索引。
- 避免使用 select *。
- 使用联合索引时,遵循最左前缀原则。
▶ Redis 有哪些数据结构?
▶ Redis 的持久化方式有哪些?
▶ Redis 的哨兵(Sentinel)模式有什么作用?
▶ Redis 的缓存穿透、缓存击穿和缓存雪崩是什么?
- 缓存穿透:查询一个不存在的数据,导致请求一直访问数据库。
- 缓存击穿:一个热点 key 过期,导致大量请求同时访问数据库。
- 缓存雪崩:大量 key 同时过期,导致大量请求同时访问数据库。
▶ Redis 的事务支持原子性吗?
▶ Redis 的过期键删除策略有哪些?
- 定期删除:每隔一段时间,随机抽取一些设置了过期时间的 key,检查是否过期,如果过期就删除。
- 惰性删除:在访问一个 key 时,先检查它是否过期,如果过期就删除。
▶ Redis 的内存淘汰策略有哪些?
▶ Redis 如何实现分布式锁?
▶ Redis 的主从复制是如何工作的?
▶ Redis Cluster 的数据分片方式是什么?
▶ Kubernetes 是什么?
▶ Kubernetes 的核心组件有哪些?
- Master 组件:kube-apiserver, kube-scheduler, kube-controller-manager, etcd.
- Node 组件:kubelet, kube-proxy, container runtime.
▶ Kubernetes 的 Pod 是什么?
▶ Kubernetes 的 Service 是什么?
▶ Kubernetes 的 Deployment 是什么?
▶ Kubernetes 的 Ingress 是什么?
▶ Kubernetes 的 ConfigMap 和 Secret 有什么区别?
- ConfigMap 用于存储非敏感的配置数据。
- Secret 用于存储敏感数据,例如密码、API 密钥等。Secret 中的数据是经过 base64 编码的。
▶ Kubernetes 的 livenessProbe 和 readinessProbe 有什么区别?
- livenessProbe 用于检测容器是否还存活。如果检测失败,kubelet 会杀死该容器,并根据重启策略来决定是否重启它。
- readinessProbe 用于检测容器是否已经准备好接收请求。如果检测失败,端点控制器会从 Service 的端点中移除该 Pod 的 IP 地址。
▶ Kubernetes 的 Helm 是什么?
▶ Kubernetes 的亲和性和反亲和性是什么?
- 亲和性:用于将 Pod 调度到满足特定条件的节点上。
- 反亲和性:用于避免将 Pod 调度到满足特定条件的节点上。
▶ Docker 是什么?
▶ Docker 的核心概念有哪些?
▶ Docker 镜像和容器有什么区别?
▶ Dockerfile 是什么?
▶ Docker 的数据卷(Volume)有什么用?
▶ Docker Compose 是什么?
▶ Docker Swarm 是什么?
▶ Docker 的网络模式有哪些?
▶ 如何清理 Docker 中的无用镜像和容器?
docker image prune:清理无用的镜像。docker container prune:清理无用的容器。docker system prune:清理所有无用的镜像、容器、网络和数据卷。
▶ Kubernetes 和 Docker Swarm 有什么区别?
- Kubernetes 是一个更强大、更复杂的容器编排工具,它提供了更丰富的功能,例如自动扩缩容、服务发现、负载均衡等。
- Docker Swarm 更简单、更容易上手,但功能相对较少。
基础
▶ init() 函数是什么时候执行的?
在main函数之前执行。
- 初始化不能采用初始化表达式初始化的变量,
- 程序运行前执行注册
- 实现sync.Once功能
- 不能被其它函数调用
- init函数没有入口参数和返回值
- 每个包可以有多个init函数,每个源文件也可以有多个init函数同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
- 不同包的init函数按照包导入的依赖关系决定执行顺序。
▶ new和make的区别?
- new只用于分配内存,返回一个指向地址的指针。它为每个新类型分配一片内存,初始化为0旦返回类型*T的内存地址,它相当于&T{}
- make只可用于slice,map,channel的初始化,返回的是引用