第三章 模型搭建和评估-评估

根据之前的模型的建模,我们知道如何运用sklearn这个库来完成建模,以及我们知道了的数据集的划分等等操作。那么一个模型我们怎么知道它好不好用呢?以至于我们能不能放心的使用模型给我的结果呢?那么今天的学习的评估,就会很有帮助。

加载下面的库

1
2
3
4
5
6
7
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from IPython.display import Image
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
1
%matplotlib inline
1
2
3
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.figsize'] = (10, 6) # 设置输出图片大小

任务:加载数据并分割测试集和训练集

1
2
3
#写入代码

from sklearn.model_selection import train_test_split
1
2
3
4
5
6
7
#写入代码
# 一般先取出X和y后再切割,有些情况会使用到未切割的,这时候X和y就可以用,x是清洗好的数据,y是我们要预测的存活数据'Survived'
data = pd.read_csv('clear_data.csv')
train = pd.read_csv('train.csv')
X = data
y = train['Survived']

1
2
3
4
#写入代码
# 对数据集进行切割
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)

1
2
3
4
5
#写入代码
# 默认参数逻辑回归模型
lr = LogisticRegression()
lr.fit(X_train, y_train)

/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(

模型评估

  • 模型评估是为了知道模型的泛化能力。
  • 交叉验证(cross-validation)是一种评估泛化性能的统计学方法,它比单次划分训练集和测试集的方法更加稳定、全面。
  • 在交叉验证中,数据被多次划分,并且需要训练多个模型。
  • 最常用的交叉验证是 k 折交叉验证(k-fold cross-validation),其中 k 是由用户指定的数字,通常取 5 或 10。
  • 准确率(precision)度量的是被预测为正例的样本中有多少是真正的正例
  • 召回率(recall)度量的是正类样本中有多少被预测为正类
  • f-分数是准确率与召回率的调和平均

【思考】:将上面的概念进一步的理解,大家可以做一下总结

1
2
3
#思考回答:


任务一:交叉验证

  • 用10折交叉验证来评估之前的逻辑回归模型
  • 计算交叉验证精度的平均值
1
2
#提示:交叉验证
Image('Snipaste_2020-01-05_16-37-56.png')
第三章模型建立和评估—评价-课程_16_0

提示4

  • 交叉验证在sklearn中的模块为sklearn.model_selection
1
2
3
#写入代码
from sklearn.model_selection import cross_val_score

1
2
3
#写入代码
lr = LogisticRegression(C=100)
scores = cross_val_score(lr, X_train, y_train, cv=10)
1
2
3
4
#写入代码
# 平均交叉验证分数
print("Average cross-validation score: {:.2f}".format(scores.mean()))

Average cross-validation score: 0.78

思考4

  • k折越多的情况下会带来什么样的影响?
1
2
3
4
5
6
#思考回答
# 当 k 越大时:
# 1. 每次训练使用的数据更多,评估偏差(bias)降低
# 2. 每次测试集样本更少,评估方差(variance)增大
# 3. 需要训练 k 个模型,计算开销显著增加

任务二:混淆矩阵

  • 计算二分类问题的混淆矩阵
  • 计算精确率、召回率以及f-分数

【思考】什么是二分类问题的混淆矩阵,理解这个概念,知道它主要是运算到什么任务中的

1
2
3
4
5
6
7
8
9
#思考回答
# 混淆矩阵(confusion matrix)是一个 2×2 的表格,用于二分类任务中展示模型预测结果与真实标签的对应关系:
# - True Positive (TP):真实为正类,预测也为正类
# - False Positive (FP):真实为负类,却被误预测为正类
# - False Negative (FN):真实为正类,却被误预测为负类
# - True Negative (TN):真实为负类,预测也为负类
# 通过混淆矩阵,可以进一步计算精确率(Precision)、召回率(Recall)、F1 分数等指标,
# 帮助我们评估模型在不同类型错误上的表现,常用于分类模型的性能评估和错误分析。

1
2
#提示:混淆矩阵
Image('Snipaste_2020-01-05_16-38-26.png')
第三章模型建立和评估—评价-课程_27_0
1
2
3
#提示:准确率 (Accuracy),精确度(Precision),Recall,f-分数计算方法
Image('Snipaste_2020-01-05_16-39-27.png')

第三章模型建立和评估—评价-课程_28_0

提示5

  • 混淆矩阵的方法在sklearn中的sklearn.metrics模块
  • 混淆矩阵需要输入真实标签和预测标签
  • 精确率、召回率以及f-分数可使用classification_report模块
1
2
3
#写入代码
from sklearn.metrics import confusion_matrix

1
2
3
4
5
#写入代码
# 训练模型
lr = LogisticRegression(C=100)
lr.fit(X_train, y_train)

/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
1
2
3
#写入代码
# 模型预测结果
pred = lr.predict(X_train)
1
2
3
4
#写入代码
# 混淆矩阵
confusion_matrix(y_train, pred)

array([[355,  57],
       [ 82, 174]])
1
2
3
from sklearn.metrics import classification_report
# 精确率、召回率以及f1-score
print(classification_report(y_train, pred))
              precision    recall  f1-score   support

           0       0.81      0.86      0.84       412
           1       0.75      0.68      0.71       256

    accuracy                           0.79       668
   macro avg       0.78      0.77      0.78       668
weighted avg       0.79      0.79      0.79       668

【思考】 * 如果自己实现混淆矩阵的时候该注意什么问题

1
2
3
4
5
6
7
8
9
#思考回答
# 如果自己实现混淆矩阵,需要注意:
# 1. 明确行列含义:通常行是真实标签,列是预测标签,并保持一致。
# 2. 类别顺序要固定:最好指定 labels 列表,避免类别稀疏时错位。
# 3. 初始化大小为 n_classes×n_classes 的零矩阵。
# 4. 索引时使用整数或统一的类别映射,避免类型不一致。
# 5. 对每个样本累加到对应的 [真实, 预测] 单元格,最后矩阵元素之和应等于样本数。
# 6. 对于未出现的类别,矩阵对应行或列应保留 0。

任务三:ROC曲线

  • 绘制ROC曲线

【思考】什么是ROC曲线,OCR曲线的存在是为了解决什么问题?

1
2
3
4
5
6
7
#思考
# ROC曲线(Receiver Operating Characteristic Curve)是一条以假正例率(FPR)为横坐标、
# 真正例率(TPR)为纵坐标绘制的曲线,展示模型在不同阈值下的分类性能变化。
# 它解决了单一阈值下评估不全面的问题,通过曲线下的面积(AUC)能够衡量模型整体区分正负样本的能力;
# 对类别不平衡更稳健,可在不同模型或参数设置间进行客观比较。


提示6

  • ROC曲线在sklearn中的模块为sklearn.metrics
  • ROC曲线下面所包围的面积越大越好
1
2
3
#写入代码
from sklearn.metrics import roc_curve

1
print(lr.decision_function(X_test))
[-1.7776276  -1.68901519 -2.9343385  -2.73339993 -0.7425476   0.1771919
  0.42300886 -0.95177507 -2.19297241 -2.09492243 -2.09876666 -2.24379328
 -0.72898893 -0.74448703  1.55206252  2.26736362 -3.0615053  -1.45551632
  1.82143942  1.10174703  2.80348253  2.20862227 -2.08595792 -1.98565326
 -2.62459231  2.61608127  2.52054836  0.46386814 -2.26805651 -1.89799476
 -4.40221097 -2.45118004 -2.11507984  0.25727282  1.56507901 -3.49922092
  0.09517543  3.1727335  -0.66659502 -2.16889122 -2.31738004 -0.75154631
  1.34173247 -0.68691348 -2.38317701 -1.48352807  3.30441868  0.37836543
  0.15120699 -2.39554116  0.71230509 -2.94049784  0.0526656  -0.12124772
  0.21937853 -0.95736671 -2.91315052  1.73227025 -2.30451919 -0.11949728
 -2.40406452 -1.23217853 -3.04709277 -2.51149884 -2.91275507  0.36741872
  1.88515182 -1.73344723  1.61180838 -2.64456699 -2.82671595 -1.32885535
 -1.89201447 -2.38194062  1.14830497  0.7324757   3.41575634 -0.04718518
  1.99047031  0.71098531 -2.5002286   2.11220527  1.35687779 -4.65208202
 -0.50164169 -2.21847127 -0.27744568 -2.1098023  -2.28203956 -2.24087733
  1.49913758 -0.46745632 -1.76590269 -3.13694507 -2.48969764 -2.52447108
 -0.31359417 -2.62456277  0.10812447 -3.22505518 -0.54301462 -1.31398633
 -2.45637232 -0.9392769  -1.99910791 -0.01952273  0.16386412  1.17043699
  0.83571934 -0.30892412 -2.56236834 -2.52630696 -2.15878988  3.38005162
 -1.63316112 -2.0470374   1.16802525  1.96428556 -0.85542758 -0.84711271
 -2.3923425  -2.27467461  1.27340371 -0.16738478  2.77379952 -0.91636487
  3.49337899  2.22265823 -1.03898765 -1.79576035  3.05405598 -1.72625544
 -2.08233698  0.14427761 -2.03826492 -1.87510703 -2.43040363  0.88364821
 -2.31722422  1.21479438 -2.19509856 -1.96948465  2.90456606  1.22909197
 -0.60993113 -2.40508898  1.79832298 -2.33619419 -1.76964851  0.54894164
  0.56920781 -1.65544357 -2.18783672 -2.51890544 -1.1167812   1.85506633
 -2.14366192  2.56003678  1.79741811  2.22038003 -0.93948297  2.11029939
  3.66773152  3.37255532 -1.62079149 -0.21922341 -2.93532548 -1.8851028
 -0.11223495 -0.89402373 -2.79168773  0.58319665 -1.20213471  2.11583429
 -1.78550619 -1.21648746 -2.91538781 -2.80005448 -2.74359191 -0.06775047
 -1.28645408 -1.17048578 -0.1176852  -1.59958242 -0.65901928 -2.40701243
  0.57575073 -3.0756839   1.53932753 -2.49031769 -3.03266822  0.30539932
 -0.05523861 -0.24431132 -2.36483723  3.25595248 -2.11664845 -1.97728592
 -2.04509461 -3.07727841 -1.11942703 -3.38920295 -2.59088459 -3.55978164
  0.22449105 -0.3214215   0.05735696  0.02061023 -3.01544378 -0.77973619
 -1.39798016 -3.10075724 -4.80621573 -3.01948006  3.44366918 -2.88193813
 -2.01992513 -0.09559774  0.91447527 -1.13270082 -2.45426968 -1.91415803
 -0.08403516]
1
2
3
4
5
6
7
8
9
10
11
#写入代码
# 计算ROC曲线的假正例率(FPR)、真正例率(TPR)和阈值
fpr, tpr, thresholds = roc_curve(y_test, lr.decision_function(X_test))
plt.plot(fpr, tpr, label="ROC Curve")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
# 找到最接近于0的阈值
close_zero = np.argmin(np.abs(thresholds))
plt.plot(fpr[close_zero], tpr[close_zero], 'o', markersize=10, label="threshold zero", fillstyle="none", c='k', mew=2)
plt.legend(loc=4)

第三章模型建立和评估—评价-课程_44_3

思考6

  • 对于多分类问题如何绘制ROC曲线
1
2
3
4
#思考回答



【思考】你能从这条OCR曲线的到什么信息?这些信息可以做什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#思考回答
# 从ROC曲线中可以得到以下信息:
# 1. 模型整体性能:曲线下的面积 (AUC - Area Under the Curve) 是一个常用的评估指标。
# - AUC = 1:完美分类器。
# - AUC = 0.5:随机分类器(无区分能力,ROC曲线接近对角线)。
# - AUC > 0.5:模型优于随机猜测。AUC越大,模型区分正负样本的能力越强。
# - AUC < 0.5:模型表现差于随机猜测(可能标签反了或者模型非常差)。
# 2. 不同阈值下的权衡:ROC曲线展示了在所有可能的分类阈值下,真正例率 (TPR) 与假正例率 (FPR) 之间的关系。
# - 曲线上的每个点代表一个特定的阈值。
# - 曲线越靠近左上角 (FPR低, TPR高),说明模型在较低的假正例率下能达到较高的真正例率,性能越好。
# 3. 模型的区分能力:曲线的形状可以反映模型区分正负样本的能力。如果曲线显著高于对角线,说明模型具有较好的区分能力。
# 4. 阈值选择的依据:可以根据业务需求,在ROC曲线上选择一个合适的平衡点(即选择一个阈值)。
# - 例如,如果更关注减少漏报(提高TPR),可以选择曲线上TPR较高的点,即使FPR可能略高。
# - 如果更关注减少误报(降低FPR),可以选择曲线上FPR较低的点,即使TPR可能略低。
# - 图中标记的 "threshold zero" 点通常是模型默认的分类阈值对应的性能点。


【回顾&引言】前面一章的内容大家可以感觉到我们主要是对基础知识做一个梳理,让大家了解数据分析的一些操作,主要做了数据的各个角度的观察。那么在这里,我们主要是做数据分析的流程性学习,主要是包括了数据清洗以及数据的特征处理,数据重构以及数据可视化。这些内容是为数据分析最后的建模和模型评价做一个铺垫。

开始之前,导入numpy、pandas包和数据

1
2
3
#加载所需的库
import numpy as np
import pandas as pd
1
2
3
#加载数据train.csv
df = pd.read_csv('./titanic/train.csv')
df.head(3)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S

2 第二章:数据清洗及特征处理

我们拿到的数据通常是不干净的,所谓的不干净,就是数据中有缺失值,有一些异常点等,需要经过一定的处理才能继续做后面的分析或建模,所以拿到数据的第一步是进行数据清洗,本章我们将学习缺失值、重复值、字符串和数据转换等操作,将数据清洗成可以分析或建模的亚子。

2.1 缺失值观察与处理

我们拿到的数据经常会有很多缺失值,比如我们可以看到Cabin列存在NaN,那其他列还有没有缺失值,这些缺失值要怎么处理呢

2.1.1 任务一:缺失值观察

  1. 请查看每个特征缺失值个数
  2. 请查看Age, Cabin, Embarked列的数据 以上方式都有多种方式,所以大家多多益善
1
2
#写入代码
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    object 
 4   Sex          891 non-null    object 
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    object 
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    object 
 11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
1
2
#写入代码
df.isnull().sum()
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age            177
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64
1
2
3
#写入代码
df[['Age','Cabin','Embarked']].head(3)
#df['Age'].head(3)
Age Cabin Embarked
0 22.0 NaN S
1 38.0 C85 C
2 26.0 NaN S

外层 [] 是 DataFrame 的索引操作符。 内层 [] 是 Python 原生的列表语法,用于传递多个列名。

2.1.2 任务二:对缺失值进行处理

(1)处理缺失值一般有几种思路

  1. 请尝试对Age列的数据的缺失值进行处理

  2. 请尝试使用不同的方法直接对整张表的缺失值进行处理

1
2
3
4
5
#处理缺失值的一般思路:
#提醒:可使用的函数有--->dropna函数与fillna函数
#print(df.head(3))
df[df['Age']==None]=0
df.head(3)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
1
2
3
#写入代码
df[df['Age'].isnull()]
df[df['Age'].isnull()] = 0
1
2
#写入代码
df[df['Age'] == np.nan] = 0

【思考1】dropna和fillna有哪些参数,分别如何使用呢?

1
2
#dropna() 是 Pandas 中用于删除包含缺失值(NaN 或 None)的行或列的函数。其核心作用是清理数据中的缺失值,适用于数据清洗阶段。
df.dropna().head(3)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
5 0 0 0 0 0 0.0 0 0 0 0.0000 0 0
1
2
#fillna() 是 Pandas 中用于填充缺失值(NaN 或 None)的核心函数,常用于数据清洗阶段。其核心作用是将缺失值替换为合理值,以便后续分析或建模。
df.fillna(0).head(3)
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 0 S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 0 S

【思考】检索空缺值用np.nan,None以及.isnull()哪个更好,这是为什么?如果其中某个方式无法找到缺失值,原因又是为什么?

【参考】https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.dropna.html

【参考】https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.fillna.html

2.2 重复值观察与处理

由于这样那样的原因,数据中会不会存在重复值呢,如果存在要怎样处理呢

2.2.1 任务一:请查看数据中的重复值

1
2
3
#写入代码
#df.duplicated()返回一个布尔序列 (Series),标记每一行是否为重复行
df[df.duplicated()]
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
17 0 0 0 0 0 0.0 0 0 0 0.0 0 0
19 0 0 0 0 0 0.0 0 0 0 0.0 0 0
26 0 0 0 0 0 0.0 0 0 0 0.0 0 0
28 0 0 0 0 0 0.0 0 0 0 0.0 0 0
29 0 0 0 0 0 0.0 0 0 0 0.0 0 0
859 0 0 0 0 0 0.0 0 0 0 0.0 0 0
863 0 0 0 0 0 0.0 0 0 0 0.0 0 0
868 0 0 0 0 0 0.0 0 0 0 0.0 0 0
878 0 0 0 0 0 0.0 0 0 0 0.0 0 0
888 0 0 0 0 0 0.0 0 0 0 0.0 0 0

176 rows × 12 columns

2.2.2 任务二:对重复值进行处理

(1)重复值有哪些处理方式呢?

(2)处理我们数据的重复值

方法多多益善

1
2
3
4
#重复值有哪些处理方式:
#删除 DataFrame 中的重复行(完全相同的行只保留一次)。
df = df.drop_duplicates()
df.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

2.2.3 任务三:将前面清洗的数据保存为csv格式

1
2
3
4
#写入代码

df.to_csv('test_clear.csv')

2.3 特征观察与处理

我们对特征进行一下观察,可以把特征大概分为两大类:
数值型特征:Survived ,Pclass, Age ,SibSp, Parch, Fare,其中Survived, Pclass为离散型数值特征,Age,SibSp, Parch, Fare为连续型数值特征
文本型特征:Name, Sex, Cabin,Embarked, Ticket,其中Sex, Cabin, Embarked, Ticket为类别型文本特征,数值型特征一般可以直接用于模型的训练,但有时候为了模型的稳定性及鲁棒性会对连续变量进行离散化。文本型特征往往需要转换成数值型特征才能用于建模分析。

2.3.1 任务一:对年龄进行分箱(离散化)处理

  1. 分箱操作是什么?

  2. 将连续变量Age平均分箱成5个年龄段,并分别用类别变量12345表示

  3. 将连续变量Age划分为[0,5) [5,15) [15,30) [30,50) [50,80)五个年龄段,并分别用类别变量12345表示

  4. 将连续变量Age按10% 30% 50% 70% 90%五个年龄段,并用分类变量12345表示

  5. 将上面的获得的数据分别进行保存,保存为csv格式

1
2
3
4
#分箱操作是什么:
'''
分箱操作(Binning)是数据预处理中的一种常用技术,主要用于将连续型数值转换为离散的区间(即“箱子”或“分组”)
'''
1
2
3
4
5
6
#写入代码
#将连续变量Age平均分箱成5个年龄段,并分别用类别变量12345表示
df['AgeBand'] = pd.cut(df['Age'], 5,labels = [1,2,3,4,5])
df.head()


PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked AgeBand
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S 2
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C 3
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S 2
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S 3
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S 3
1
2
3
4
5
#写入代码
#将连续变量Age划分为(0,5] (5,15] (15,30] (30,50] (50,80]五个年龄段,并分别用类别变量12345表示
df['AgeBand'] = pd.cut(df['Age'],[0,5,15,30,50,80],labels = [1,2,3,4,5])
df.head(3)

PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked AgeBand
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S 3
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C 4
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S 3
1
2
3
4
#写入代码
#将连续变量Age按10% 30% 50 70% 90%五个年龄段,并用分类变量12345表示
df['AgeBand'] = pd.qcut(df['Age'],[0,0.1,0.3,0.5,0.7,0.9],labels = [1,2,3,4,5])
df.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked AgeBand
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S 2
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C 5
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S 3
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S 4
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S 4

【参考】https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.cut.html

【参考】https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.qcut.html

2.3.2 任务二:对文本变量进行转换

  1. 查看文本变量名及种类
  2. 将文本变量Sex, Cabin ,Embarked用数值变量12345表示
  3. 将文本变量Sex, Cabin, Embarked用one-hot编码表示
1
2
3
4
5
6
#写入代码
#方法一: value_counts
print(
df['Sex'].value_counts(),
df['Cabin'].value_counts(),
df['Embarked'].value_counts())
Sex
male      453
female    261
0           1
Name: count, dtype: int64 Cabin
B96 B98        4
G6             4
C23 C25 C27    4
F2             3
C22 C26        3
              ..
E36            1
D7             1
C118           1
C99            1
D37            1
Name: count, Length: 135, dtype: int64 Embarked
S    554
C    130
Q     28
0      1
Name: count, dtype: int64
1
2
3
#写入代码
df['Sex'].unique()
df['Sex'].nunique()
3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#写入代码
#将类别文本转换为12345

#方法一: replace
df['Sex_num'] = df['Sex'].replace(['male','female'],[1,2])
df.head()
#方法二: map
df['Sex_num'] = df['Sex'].map({'male': 1, 'female': 2})
df.head()
#方法三: 使用sklearn.preprocessing的LabelEncoder
from sklearn.preprocessing import LabelEncoder
for feat in ['Cabin', 'Ticket']:
lbl = LabelEncoder()
label_dict = dict(zip(df[feat].unique(), range(df[feat].nunique())))
#print(label_dict)
df[feat + "_labelEncode"] = df[feat].map(label_dict)
df[feat + "_labelEncode"] = lbl.fit_transform(df[feat].astype(str))

df.head()

/tmp/ipykernel_1400/2627332835.py:5: FutureWarning: Downcasting behavior in `replace` is deprecated and will be removed in a future version. To retain the old behavior, explicitly call `result.infer_objects(copy=False)`. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
  df['Sex_num'] = df['Sex'].replace(['male','female'],[1,2])
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Age_66.0 Age_70.0 Age_70.5 Age_71.0 Age_74.0 Age_80.0 Embarked_0 Embarked_C Embarked_Q Embarked_S
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 False False False False False False False False False True
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 False False False False False False False True False False
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 False False False False False False False False False True
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 False False False False False False False False False True
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 False False False False False False False False False True

5 rows × 109 columns

1
2
3
4
5
6
7
8
9
10
11
12
#将类别文本转换为one-hot编码

#方法一: OneHotEncoder
for feat in ["Age", "Embarked"]:
x = pd.get_dummies(df["Age"] // 6)
# x = pd.get_dummies(pd.cut(df['Age'],5))
x = pd.get_dummies(df[feat], prefix=feat)
df = pd.concat([df, x], axis=1)
#df[feat] = pd.get_dummies(df[feat], prefix=feat)

df.head()
df.to_csv('temp.csv')

2.3.3 任务三:从纯文本Name特征里提取出Titles的特征(所谓的Titles就是Mr,Miss,Mrs等)

1
2
3
4
#写入代码



1
2
#保存最终你完成的已经清理好的数据

第三章 模型搭建和评估–建模

经过前面的两章的知识点的学习,我可以对数数据的本身进行处理,比如数据本身的增删查补,还可以做必要的清洗工作。那么下面我们就要开始使用我们前面处理好的数据了。这一章我们要做的就是使用数据,我们做数据分析的目的也就是,运用我们的数据以及结合我的业务来得到某些我们需要知道的结果。那么分析的第一步就是建模,搭建一个预测模型或者其他模型;我们从这个模型的到结果之后,我们要分析我的模型是不是足够的可靠,那我就需要评估这个模型。今天我们学习建模,下一节我们学习评估。

我们拥有的泰坦尼克号的数据集,那么我们这次的目的就是,完成泰坦尼克号存活预测这个任务。

1
2
3
4
5
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.display import Image
1
%matplotlib inline
1
2
3
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.figsize'] = (10, 6) # 设置输出图片大小

载入这些库,如果缺少某些库,请安装他们

【思考】这些库的作用是什么呢?你需要查一查

1
2
3
4
5
6
#思考题回答
'''
Image 是 IPython.display 模块的一部分,用于在 Jupyter Notebook 或 IPython 环境中直接显示图像。支持从本地路径或网络 URL 加载图像,并控制显示格式(如宽度、高度、格式类型)
seaborn 是一个基于 Matplotlib 的高级数据可视化库,专注于统计图形的绘制(如分布图、相关性图、分类图等),能快速生成美观且信息丰富的图表
'''

'\nImage 是 IPython.display 模块的一部分,用于在 Jupyter Notebook 或 IPython 环境中直接显示图像。支持从本地路径或网络 URL 加载图像,并控制显示格式(如宽度、高度、格式类型)\nseaborn 是一个基于 Matplotlib 的高级数据可视化库,专注于统计图形的绘制(如分布图、相关性图、分类图等),能快速生成美观且信息丰富的图表\n'
1
%matplotlib inline

载入我们提供清洗之后的数据(clear_data.csv),大家也将原始数据载入(train.csv),说说他们有什么不同

1
2
3
#写入代码
train = pd.read_csv('train.csv')
train.shape
(891, 12)
1
2
#写入代码
train.head()
.dataframe tbody tr th {
    vertical-align: top;
}

.dataframe thead th {
    text-align: right;
}
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
1
2
3
4
#写入代码
data = pd.read_csv('clear_data.csv')

data.head()
PassengerId Pclass Age SibSp Parch Fare Sex_female Sex_male Embarked_C Embarked_Q Embarked_S
0 0 3 22.0 1 0 7.2500 0 1 0 0 1
1 1 1 38.0 1 0 71.2833 1 0 1 0 0
2 2 3 26.0 0 0 7.9250 1 0 0 0 1
3 3 1 35.0 1 0 53.1000 1 0 0 0 1
4 4 3 35.0 0 0 8.0500 0 1 0 0 1

模型搭建

  • 处理完前面的数据我们就得到建模数据,下一步是选择合适模型
  • 在进行模型选择之前我们需要先知道数据集最终是进行监督学习还是无监督学习
  • 模型的选择一方面是通过我们的任务来决定的。
  • 除了根据我们任务来选择模型外,还可以根据数据样本量以及特征的稀疏性来决定
  • 刚开始我们总是先尝试使用一个基本的模型来作为其baseline,进而再训练其他模型做对比,最终选择泛化能力或性能比较好的模型

这里我的建模,并不是从零开始,自己一个人完成完成所有代码的编译。我们这里使用一个机器学习最常用的一个库(sklearn)来完成我们的模型的搭建

下面给出sklearn的算法选择路径,供大家参考

1
2
# sklearn模型算法选择路径图
Image('sklearn.png')
第三章模型建立和评估–建模-课程_17_0

【思考】数据集哪些差异会导致模型在拟合数据是发生变化

1
2
3
#思考回答


任务一:切割训练集和测试集

这里使用留出法划分数据集

  • 将数据集分为自变量和因变量
  • 按比例切割训练集和测试集(一般测试集的比例有30%、25%、20%、15%和10%)
  • 使用分层抽样
  • 设置随机种子以便结果能复现

【思考】 * 划分数据集的方法有哪些? * 为什么使用分层抽样,这样的好处有什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 划分数据集的方法有哪些?
# 1. **留出法 (Hold-out Cross Validation)**:
# * 直接将数据集D划分为两个互斥的集合:训练集S和测试集T。
# * 例如,70%的数据用于训练,30%用于测试。
# * 优点:简单、计算开销小。
# * 缺点:划分具有随机性,单次划分的结果可能不够稳定和准确,尤其是在数据集较小时。训练集和测试集的样本比例会影响评估结果。

# 2. **交叉验证法 (Cross Validation)**:
# * **k折交叉验证 (k-Fold Cross Validation)**:
# * 将数据集D划分为k个大小相似的互斥子集 D1, D2, ..., Dk。
# * 每次用k-1个子集的并集作为训练集,余下的那个子集作为测试集。
# * 这样可以获得k组训练/测试集,从而可进行k次训练和测试,最终返回的是这k个测试结果的均值。
# * 常用的k值为5或10。
# * 优点:比留出法更稳定,更充分地利用了数据。
# * 缺点:计算开销是k倍。
# * **留一法 (Leave-One-Out Cross Validation, LOOCV)**:
# * k折交叉验证的特例,当k等于样本数N时。
# * 每次只留下一个样本作为测试集,其余N-1个样本作为训练集。
# * 优点:评估结果通常被认为比较准确,因为几乎所有数据都用于训练。
# * 缺点:计算开销非常大,尤其是在数据集很大时。

# 3. **自助法 (Bootstrapping)**:
# * 以自助采样法为基础。给定包含m个样本的数据集D,对它进行采样产生数据集D':每次随机从D中挑选一个样本,将其拷贝放入D',然后再将该样本放回初始数据集D中,使得该样本在下次采样时仍有可能被采到。这个过程重复执行m次后,我们就得到了包含m个样本的数据集D'。
# * 可以证明,初始数据集D中约有36.8%的样本未出现在采样数据集D'中。于是我们可将D'用作训练集,D\D'用作测试集。
# * 优点:在数据集较小、难以有效划分训练/测试集时很有用;能从初始数据集中产生多个不同的训练集。
# * 缺点:自助法产生的数据集改变了初始数据集的分布,会引入估计偏差。

# 为什么使用分层抽样,这样的好处有什么?
# **分层抽样 (Stratified Sampling)** 是一种抽样技术,它将总体(数据集)划分为若干个互不重叠的子群(称为“层”),然后从每个层中独立地进行简单随机抽样。在划分训练集和测试集时,特别是对于分类任务,通常是根据目标变量的类别进行分层。

# **好处:**
# 1. **保持类别比例一致性**:
# * 确保训练集和测试集中的各个类别的样本比例与原始数据集中各个类别的样本比例大致相同。
# * 这对于类别不平衡的数据集尤为重要。如果进行纯随机抽样,可能会导致训练集或测试集中某些少数类别的样本过少,甚至没有,从而影响模型的训练效果和评估的可靠性。
# 2. **提高模型的泛化能力和评估的准确性**:
# * 由于训练集和测试集都较好地代表了原始数据的类别分布,模型在训练时能学习到各个类别的特征,评估时也能更准确地反映模型在所有类别上的表现。
# * 避免了因随机划分导致训练集和测试集在类别分布上产生较大差异,从而使得模型评估结果更加稳定和可信。
# 3. **减少抽样误差**:
# * 相比于简单随机抽样,分层抽样通常能得到更具代表性的样本,从而减少因抽样带来的误差,使得基于样本的推断更加精确。

任务提示1

  • 切割数据集是为了后续能评估模型泛化能力
  • sklearn中切割数据集的方法为train_test_split
  • 查看函数文档可以在jupyter noteboo里面使用train_test_split?后回车即可看到
  • 分层和随机种子在参数里寻找

要从clear_data.csv和train.csv中提取train_test_split()所需的参数

1
2
3
4
5
6
7
#写入代码
from sklearn.model_selection import train_test_split
# 一般先取出X和y后再切割,有些情况会使用到未切割的,这时候X和y就可以用,x是清洗好的数据,y是我们要预测的存活数据'Survived'
X = data
y = train['Survived']
# 对数据集进行切割
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=0)
1
2
3
4
#写入代码

X_train.shape, X_test.shape

((668, 11), (223, 11))

【思考】 * 什么情况下切割数据集的时候不用进行随机选取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#思考回答
# 在以下情况下切割数据集时可能不需要或不适合进行随机选取:

# 1. **时间序列数据 (Time Series Data)**:
# * 对于时间序列数据,数据的顺序至关重要,因为它包含了时间依赖性。随机打乱顺序会破坏这种依赖关系。
# * 通常的做法是按时间顺序划分,例如,将较早的数据作为训练集,较晚的数据作为测试集(或验证集)。这更符合实际应用中用过去预测未来的场景。
# * 例如,用前几年的股票数据训练模型,用最近一年的数据测试模型。

# 2. **数据已经预先排序或具有特定结构**:
# * 如果数据集已经按照某种对分析有意义的顺序排列(例如,按地理区域、按实验批次等),并且你希望测试集来自与训练集不同的、特定的部分,那么可能需要按顺序或按特定规则划分,而不是随机划分。
# * 例如,在一个全国性的调查数据中,你可能想用某些省份的数据做训练,用另一些省份的数据做测试,以检验模型的地域泛化能力。

# 3. **数据集非常大且分布均匀**:
# * 当数据集非常庞大,并且可以合理假设数据是独立同分布 (i.i.d.) 且分布均匀时,简单地按顺序取一部分作为训练集,另一部分作为测试集,其效果可能与随机选取相差不大。随机选取的计算开销在这种情况下可能显得不必要。
# * 然而,即使在这种情况下,随机选取通常仍然是更稳妥的做法,以避免潜在的未知偏差。

# 4. **特定的交叉验证策略**:
# * 某些交叉验证方法本身就定义了非随机的划分方式。例如,在k折交叉验证中,虽然整体上数据被分成了k折,但每一折的选择是确定的(通常是按顺序分割)。留一法交叉验证更是每次只留一个特定的样本作为测试集。

# 5. **当需要完全复现特定的、非随机的划分结果时**:
# * 如果之前的研究或实验使用了某种特定的非随机划分方式,为了比较或复现结果,也需要采用相同的划分方式。

# 6. **流式数据或在线学习场景**:
# * 在数据持续不断流入的场景中,模型可能需要用新到达的数据进行测试或持续训练。这种情况下,测试集自然是最新的一部分数据,而不是从历史数据中随机抽取的。



任务二:模型创建

  • 创建基于线性模型的分类模型(逻辑回归)
  • 创建基于树的分类模型(决策树、随机森林)
  • 分别使用这些模型进行训练,分别的到训练集和测试集的得分
  • 查看模型的参数,并更改参数值,观察模型变化

提示

  • 逻辑回归不是回归模型而是分类模型,不要与LinearRegression混淆
  • 随机森林其实是决策树集成为了降低决策树过拟合的情况
  • 线性模型所在的模块为sklearn.linear_model
  • 树模型所在的模块为sklearn.ensemble
1
2
3
4
5
#写入代码

from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier

1
2
3
4
5
6
#写入代码
# 默认参数逻辑回归模型
lr = LogisticRegression()
lr.fit(X_train, y_train)


/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
1
2
3
4
5
6
#写入代码
# 查看训练集和测试集score值
print("Training set score: {:.2f}".format(lr.score(X_train, y_train)))
print("Testing set score: {:.2f}".format(lr.score(X_test, y_test)))


Training set score: 0.80
Testing set score: 0.79
1
2
3
4
5
6
7
#写入代码
# 调整参数后的逻辑回归模型
lr2 = LogisticRegression(C=100)
lr2.fit(X_train, y_train)
print("Training set score: {:.2f}".format(lr2.score(X_train, y_train)))
print("Testing set score: {:.2f}".format(lr2.score(X_test, y_test)))

Training set score: 0.79
Testing set score: 0.78


/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/sklearn/linear_model/_logistic.py:465: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT.

Increase the number of iterations (max_iter) or scale the data as shown in:
    https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
    https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
  n_iter_i = _check_optimize_result(
1
2
3
4
5
# 默认参数的随机森林分类模型
rfc = RandomForestClassifier()
rfc.fit(X_train, y_train)
print("Training set score: {:.2f}".format(rfc.score(X_train, y_train)))
print("Testing set score: {:.2f}".format(rfc.score(X_test, y_test)))
Training set score: 1.00
Testing set score: 0.82

【思考】 * 为什么线性模型可以进行分类任务,背后是怎么的数学关系 * 对于多分类问题,线性模型是怎么进行分类的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#思考回答
# 为什么线性模型可以进行分类任务,背后是怎么的数学关系
# 线性模型(如逻辑回归 Logistic Regression,或者支持向量机 SVM 的线性核)之所以能用于分类任务,是因为它们通过以下方式将线性组合的输入特征映射到类别预测:
# 1. **线性组合**:首先,模型计算输入特征的线性组合,形式通常为 `z = w_1*x_1 + w_2*x_2 + ... + w_n*x_n + b`,或者用向量表示为 `z = w^T * x + b`。
# * `x` 是输入特征向量。
# * `w` 是模型学习到的权重(或系数)。
# * `b` 是偏置项(或截距)。
# 这个 `z` 值可以看作是样本点到决策边界的某种度量。
# 2. **决策函数/激活函数**:然后,这个线性组合的结果 `z` 会被传递给一个决策函数或激活函数,该函数将其转换为类别预测或类别概率。
# * **对于逻辑回归 (Logistic Regression)**:
# * 它使用 Sigmoid (Logistic) 函数:`p = 1 / (1 + e^(-z))`。
# * Sigmoid 函数将任意实数值 `z` 映射到 (0, 1) 区间,这个输出 `p` 可以解释为样本属于正类(通常是类别1)的概率。
# * 通过设定一个阈值(通常是0.5),如果 `p > 0.5` (即 `z > 0`),则预测为正类;否则预测为负类。
# * 因此,决策边界是 `z = 0`,即 `w^T * x + b = 0`,这是一个超平面。
# * **对于线性支持向量机 (Linear SVM)**:
# * 它直接使用 `z` 的符号来决定类别。如果 `z > 0`,预测为一类;如果 `z < 0`,预测为另一类。
# * SVM 的目标是找到一个能最大化两类样本之间间隔(margin)的决策边界(超平面)。
# 总结来说,线性模型通过学习一个线性决策边界(直线、平面或超平面)来分隔不同类别的样本。它们首先计算一个线性得分,然后通过一个非线性函数(如Sigmoid)或直接根据得分的符号来做出分类决策。

# 对于多分类问题,线性模型是怎么进行分类的
# 当类别数量大于两个时(即多分类问题),线性模型通常采用以下两种主要策略之一将问题转化为多个二分类问题:
# 1. **一对余 (One-vs-Rest, OvR) 或 一对所有 (One-vs-All, OvA)**:
# * **原理**:如果有个 `K` 个类别,OvR 策略会训练 `K` 个独立的二分类器。
# * 第 `i` 个分类器 (`i` 从 1 到 `K`) 会将类别 `i` 的样本视为正类,而将所有其他 `K-1` 个类别的样本视为负类。
# * **预测**:对于一个新的样本,它会被输入到所有 `K` 个分类器中。每个分类器都会输出一个分数或概率,表示该样本属于其对应“正类”的置信度。最终,样本被分配给那个给出最高置信度分数的类别。
# * **优点**:直观,实现相对简单,计算效率较高(只需要训练K个分类器)。
# * **缺点**:当类别数量很多时,每个二分类器的负类可能包含非常多样化的样本,可能导致类别不平衡问题。

# 2. **一对一 (One-vs-One, OvO)**:
# * **原理**:如果有个 `K` 个类别,OvO 策略会为每一对类别 `(i, j)` 训练一个二分类器,其中 `i != j`。总共需要训练 `K * (K-1) / 2` 个分类器。
# * 每个分类器只负责区分两个特定的类别。
# * **预测**:对于一个新的样本,它会被输入到所有 `K * (K-1) / 2` 个分类器中。每个分类器都会对样本属于其两个类别中的哪一个进行投票。最终,样本被分配给获得最多投票的类别。
# * **优点**:每个分类器只需要处理两个类别的数据,通常训练速度更快,且对于某些对类别不平衡不敏感的算法(如SVM)可能表现更好。
# * **缺点**:当类别数量 `K` 很大时,需要训练的分类器数量会急剧增加,导致计算成本和存储成本较高。

# **在 scikit-learn 中**:
# * `LogisticRegression` 默认使用 OvR 策略进行多分类 (可以通过 `multi_class` 参数设置为 `'multinomial'` 来使用 Softmax 回归,这是一种直接处理多分类的方法,但其基础仍然是线性的)。
# * `LinearSVC` (线性支持向量机) 默认使用 OvR 策略。


任务三:输出模型预测结果

  • 输出模型预测分类标签
  • 输出不同分类标签的预测概率

提示3

  • 一般监督模型在sklearn里面有个predict能输出预测标签,predict_proba则可以输出标签概率
1
2
#写入代码
pred = lr.predict(X_train)
1
2
#写入代码
pred[:10]
array([0, 1, 1, 1, 0, 0, 1, 0, 1, 1])
1
2
#写入代码
pred_proba = lr.predict_proba(X_train)
1
2
#写入代码
pred_proba[:10]
array([[0.60887905, 0.39112095],
       [0.17668722, 0.82331278],
       [0.40624596, 0.59375404],
       [0.18896449, 0.81103551],
       [0.87984221, 0.12015779],
       [0.91385758, 0.08614242],
       [0.13282516, 0.86717484],
       [0.90555878, 0.09444122],
       [0.05280619, 0.94719381],
       [0.10934565, 0.89065435]])

【思考】 * 预测标签的概率对我们有什么帮助

1
2
3
4
#思考回答



复习:回顾学习完第一章,我们对泰坦尼克号数据有了基本的了解,也学到了一些基本的统计方法,第二章中我们学习了数据的清理和重构,使得数据更加的易于理解;今天我们要学习的是第二章第三节:数据可视化,主要给大家介绍一下Python数据可视化库Matplotlib,在本章学习中,你也许会觉得数据很有趣。在打比赛的过程中,数据可视化可以让我们更好的看到每一个关键步骤的结果如何,可以用来优化方案,是一个很有用的技巧。

2 第二章:数据可视化

开始之前,导入numpy、pandas以及matplotlib包和数据

1
2
3
4
5
6
# 加载所需的库
# 如果出现 ModuleNotFoundError: No module named 'xxxx'
# 你只需要在终端/cmd下 pip install xxxx 即可
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
1
2
3
#加载result.csv这个数据
text = pd.read_csv(r'result.csv')
text.head()
Unnamed: 0 PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

2.7 如何让人一眼看懂你的数据?

《Python for Data Analysis》第九章

2.7.1 任务一:跟着书本第九章,了解matplotlib,自己创建一个数据项,对其进行基本可视化

【思考】最基本的可视化图案有哪些?分别适用于那些场景?(比如折线图适合可视化某个属性值随时间变化的走势)

1
2
3
#思考回答
#这一部分需要了解可视化图案的的逻辑,知道什么样的图案可以表达什么样的信号b

2.7.2 任务二:可视化展示泰坦尼克号数据集中男女中生存人数分布情况(用柱状图试试)。

1
2
3
4
5
6
#代码编写
sex = text.groupby('Sex')['Survived'].sum()
sex.plot.bar()
plt.title('survived_count')
plt.show()

第二章:第四节数据可视化-课程_10_0

【思考】计算出泰坦尼克号数据集中男女中死亡人数,并可视化展示?如何和男女生存人数可视化柱状图结合到一起?看到你的数据可视化,说说你的第一感受(比如:你一眼看出男生存活人数更多,那么性别可能会影响存活率)。

1
2
3
#思考题回答


2.7.3 任务三:可视化展示泰坦尼克号数据集中男女中生存人与死亡人数的比例图(用柱状图试试)。

1
2
3
4
5
6
7
#代码编写
# 提示:计算男女中死亡人数 1表示生存,0表示死亡
text.groupby(['Sex','Survived'])['Survived'].count().unstack().plot(kind='bar',stacked='True')
plt.title('survived_count')
plt.ylabel('count')


Text(0, 0.5, 'count')
第二章:第四节数据可视化-课程_14_1

【提示】男女这两个数据轴,存活和死亡人数按比例用柱状图表示

2.7.4 任务四:可视化展示泰坦尼克号数据集中不同票价的人生存和死亡人数分布情况。(用折线图试试)(横轴是不同票价,纵轴是存活人数)

【提示】对于这种统计性质的且用折线表示的数据,你可以考虑将数据排序或者不排序来分别表示。看看你能发现什么?

1
2
3
4
5
6
7
#代码编写
# 计算不同票价中生存与死亡人数 1表示生存,0表示死亡
#print(text.groupby(['Fare'])['Survived'].value_counts())
fare_sur = text.groupby(['Fare'])['Survived'].value_counts().sort_values(ascending=False)
fare_sur


Fare     Survived
8.0500   0           38
7.8958   0           37
13.0000  0           26
7.7500   0           22
26.0000  0           16
                     ..
6.9500   0            1
6.9750   0            1
         1            1
7.0458   0            1
7.1417   1            1
Name: count, Length: 330, dtype: int64
1
2
3
4
5
# 排序后绘折线图
fig = plt.figure(figsize=(20, 18))
fare_sur.plot(grid=True)
plt.legend()
plt.show()
第二章:第四节数据可视化-课程_19_0
1
2
3
4
5
6
7
8
# 写入代码
# 排序前绘折线图
fare_sur1 = text.groupby(['Fare'])['Survived'].value_counts()
fare_sur1
fig = plt.figure(figsize=(20, 18))
fare_sur1.plot(grid=True)
plt.legend()
plt.show()
第二章:第四节数据可视化-课程_20_0

2.7.5 任务五:可视化展示泰坦尼克号数据集中不同仓位等级的人生存和死亡人员的分布情况。(用柱状图试试)

1
2
3
4
#代码编写
# 1表示生存,0表示死亡
pclass_sur = text.groupby(['Pclass'])['Survived'].value_counts()
pclass_sur
Pclass  Survived
1       1           136
        0            80
2       0            97
        1            87
3       0           372
        1           119
Name: count, dtype: int64
1
2
import seaborn as sns
sns.countplot(x="Pclass", hue="Survived", data=text)
<Axes: xlabel='Pclass', ylabel='count'>
第二章:第四节数据可视化-课程_23_1

【思考】看到这个前面几个数据可视化,说说你的第一感受和你的总结

1
2
3
#思考题回答


2.7.6 任务六:可视化展示泰坦尼克号数据集中不同年龄的人生存与死亡人数分布情况。(不限表达方式)

1
2
3
4
5
#代码编写
facet = sns.FacetGrid(text, hue="Survived",aspect=3)
facet.map(sns.kdeplot,'Age',shade= True)
facet.set(xlim=(0, text['Age'].max()))
facet.add_legend()
/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/seaborn/axisgrid.py:854: FutureWarning: 

`shade` is now deprecated in favor of `fill`; setting `fill=True`.
This will become an error in seaborn v0.14.0; please update your code.

  func(*plot_args, **plot_kwargs)
/root/.pyenv/versions/3.11.1/lib/python3.11/site-packages/seaborn/axisgrid.py:854: FutureWarning: 

`shade` is now deprecated in favor of `fill`; setting `fill=True`.
This will become an error in seaborn v0.14.0; please update your code.

  func(*plot_args, **plot_kwargs)

<seaborn.axisgrid.FacetGrid at 0x7f9c6bce1f50>
第二章:第四节数据可视化-课程_27_2

2.7.7 任务七:可视化展示泰坦尼克号数据集中不同仓位等级的人年龄分布情况。(用折线图试试)

1
2
3
4
5
6
7
#代码编写

text.Age[text.Pclass == 1].plot(kind='kde')
text.Age[text.Pclass == 2].plot(kind='kde')
text.Age[text.Pclass == 3].plot(kind='kde')
plt.xlabel("age")
plt.legend((1,2,3),loc="best")
<matplotlib.legend.Legend at 0x7f9c69946e90>
第二章:第四节数据可视化-课程_29_1

【思考】上面所有可视化的例子做一个总体的分析,你看看你能不能有自己发现

【总结】到这里,我们的可视化就告一段落啦,如果你对数据可视化极其感兴趣,你还可以了解一下其他可视化模块,如:pyecharts,bokeh等。

如果你在工作中使用数据可视化,你必须知道数据可视化最大的作用不是炫酷,而是最快最直观的理解数据要表达什么,你觉得呢?

复习:在前面我们已经学习了Pandas基础,第二章我们开始进入数据分析的业务部分,在第二章第一节的内容中,我们学习了数据的清洗,这一部分十分重要,只有数据变得相对干净,我们之后对数据的分析才可以更有力。而这一节,我们要做的是数据重构,数据重构依旧属于数据理解(准备)的范围。

开始之前,导入numpy、pandas包和数据

1
2
3
# 导入基本库
import numpy as np
import pandas as pd
1
2
3
# 载入data文件中的:train-left-up.csv
text=pd.read_csv('../第二章项目集合/data/train-left-up.csv')
text.head()
PassengerId Survived Pclass Name
0 1 0 3 Braund, Mr. Owen Harris
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th…
2 3 1 3 Heikkinen, Miss. Laina
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel)
4 5 0 3 Allen, Mr. William Henry

2 第二章:数据重构

2.4 数据的合并

2.4.1 任务一:将data文件夹里面的所有数据都载入,观察数据的之间的关系

1
2
3
4
5
6
7
#写入代码
text_left_up=pd.read_csv('../第二章项目集合/data/train-left-up.csv')
text_left_down=pd.read_csv('../第二章项目集合/data/train-left-down.csv')
text_right_up=pd.read_csv('../第二章项目集合/data/train-right-up.csv')
text_right_down=pd.read_csv('../第二章项目集合/data/train-right-down.csv')
text_left_up.head()

PassengerId Survived Pclass Name
0 1 0 3 Braund, Mr. Owen Harris
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th…
2 3 1 3 Heikkinen, Miss. Laina
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel)
4 5 0 3 Allen, Mr. William Henry

【提示】结合之前我们加载的train.csv数据,大致预测一下上面的数据是什么

2.4.2:任务二:使用concat方法:将数据train-left-up.csv和train-right-up.csv横向合并为一张表,并保存这张表为result_up

1
2
3
4
5
#写入代码
#pandas.concat 是 Pandas 中用于连接 Series 或 DataFrame 对象的核心方法,支持横向(列方向)或纵向(行方向)拼接
list_up = [text_left_up,text_right_up]
result_up = pd.concat(list_up,axis=1)
result_up.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

2.4.3 任务三:使用concat方法:将train-left-down和train-right-down横向合并为一张表,并保存这张表为result_down。然后将上边的result_up和result_down纵向合并为result。

1
2
3
4
5
6
#写入代码
list_down=[text_left_down,text_right_down]
result_down = pd.concat(list_down,axis=1)
result = pd.concat([result_up,result_down])
result.head()

PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

2.4.4 任务四:使用DataFrame自带的方法join方法和append:完成任务二和任务三的任务

1
2
3
4
5
#写入代码
resul_up = text_left_up.join(text_right_up)
result_down = text_left_down.join(text_right_down)
result = result_up.append(result_down)
result.head()

2.4.5 任务五:使用Panads的merge方法和DataFrame的append方法:完成任务二和任务三的任务

1
2
3
4
5
6
7
8
9
#写入代码
'''
该代码使用 pandas.merge 方法,以索引(index)为键,将两个 DataFrame (text_left_up 和 text_right_up) 横向合并。
'''
result_up = pd.merge(text_left_up,text_right_up,left_index=True,right_index=True)
result_down = pd.merge(text_left_down,text_right_down,left_index=True,right_index=True)
result = resul_up.append(result_down)
result.head()

【思考】对比merge、join以及concat的方法的不同以及相同。思考一下在任务四和任务五的情况下,为什么都要求使用DataFrame的append方法,如何只要求使用merge或者join可不可以完成任务四和任务五呢?

2.4.6 任务六:完成的数据保存为result.csv

1
2
3
4
#写入代码

result.to_csv('result.csv')
result.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

2.5 换一种角度看数据

2.5.1 任务一:将我们的数据变为Series类型的数据

1
2
3
4
5
#写入代码
#text.stack() 是 Pandas 中用于将 DataFrame 的列旋转为行的方法
text = pd.read_csv('result.csv')
unit_result=text.stack().head(20)
unit_result.head()
0  Unnamed: 0                           0
   PassengerId                          1
   Survived                             0
   Pclass                               3
   Name           Braund, Mr. Owen Harris
dtype: object
1
2
3
#写入代码

unit_result.to_csv('unit_result.csv')

前言

代码仓库zxj-2023/learn_fastapi

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,使用 Python 并基于标准的 Python 类型提示。

关键特性:

  • 快速:可与 NodeJSGo 并肩的极高性能(归功于 Starlette 和 Pydantic)。最快的 Python web 框架之一
  • 高效编码:提高功能开发速度约 200% 至 300%。*
  • 更少 bug:减少约 40% 的人为(开发者)导致错误。*
  • 智能:极佳的编辑器支持。处处皆可自动补全,减少调试时间。
  • 简单:设计的易于使用和学习,阅读文档的时间更短。
  • 简短:使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。
  • 健壮:生产可用级别的代码。还有自动生成的交互式文档。
  • 标准化:基于(并完全兼容)API 的相关开放标准:OpenAPI (以前被称为 Swagger) 和 JSON Schema

两个核心组件:Starlette 和 Pydantic

Starlette 负责web部分

Starlette 是 FastAPI 的底层 ASGI(异步服务器网关接口)框架,为 FastAPI 提供了异步编程能力和高性能的网络通信支持。

ASGI(Asynchronous Server Gateway Interface )是一种用于连接 Python Web 服务器和应用程序框架的异步接口标准 ,旨在支持现代 Web 协议(如 WebSocket、HTTP/2)和异步编程模型

Pydantic负责

Pydantic 负责 FastAPI 的数据验证、序列化和自动文档生成

http协议

一、简介

HTTP协议 是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于万维网(WWW: World Wide Web)服务器与本地浏览器之间传输超文本的传送协议。HTTP是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展。HTTP协议工作于客户端-服务端架构为上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。Web服务器根据接收到的请求后,向客户端发送响应信息。

1

二、http协议特性

(1)基于 TCP/IP 协议

http 协议是基于 TCP/IP 协议之上的应用层协议。

(2)基于请求 - 响应模式

HTTP 协议规定,请求从客户端发出,最后服务器端响应应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应

(3)无状态保存

HTTP 是一种不保存状态,即无状态(stateless)协议。HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。

使用 HTTP 协议,每当有新的请求发送时,就会有对应的新响应产生。协议本身并不保留之前一切的请求或响应报文的信息。这是为了更快地处理大量事务,确保协议的可伸缩性,而特意把 HTTP 协议设计成如此简单的。

(4)短连接

HTTP 1.0 默认使用的是短连接。浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。

HTTP 1.1 起,默认使用长连接。要使用长连接,客户端和服务器的 HTTP 首部的 Connection 都要设置为 keep - alive,才能支持长连接。

HTTP 长连接,指的是复用 TCP 连接。多个 HTTP 请求可以复用同一个 TCP 连接,这就节省了 TCP 连接建立和断开的消耗。

三、http请求协议与响应协议

2

Socket(套接字)是计算机网络中用于实现进程间双向通信的端点抽象,它为应用层进程通过网络协议交换数据提供了统一的接口。具体来说,Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,本质上是一组封装了复杂网络协议的接口,简化了开发者对底层通信细节的操作。

从功能上看,Socket 可以看作是网络通信的“电话插座”:两个设备(如客户端与服务器)通过 Socket 建立连接后,即可像电话通话一样进行数据交换,而端口号则类似于插座上的插孔,用于标识具体的通信进程,且不能被其他进程占用。此外,Socket 包含网络通信必需的五种核心信息,例如使用的协议(TCP/UDP)、本地与远程地址、端口等,构成了网络通信的基本操作单元。

总结而言,Socket 既是通信端点的逻辑概念,也是实现网络应用层交互的关键工具,其设计目标是屏蔽底层协议的复杂性,提供统一的编程接口。

3

GET :请求参数通过 URL 的查询字符串(Query String)传递,数据暴露在地址栏中,例如:https://example.com ?name=value

POST :请求参数存储在请求体(Body)中传输,相对更安全,且支持传输非字符串数据(如文件、二进制等)

一个完整的URL包括:协议、ip、端口、路径、参数

例如:https://www.baidu.com/s?wd=yuan 其中https是协议,www.baidu.com 是IP,端口默认80,/s是路径,参数是wd=yuan

请求方式:get与post请求

  • GET提交的数据会放在URL之后,以?分割URL和传输数据,参数之间以&相连,如EditBook?name=test1&id=123456。POST方法是把提交的数据放在HTTP包的请求体中。
  • GET提交的数据大小有限制(因为浏览器对URL的长度有限制),而POST方法提交的数据没有限制

响应状态码:状态码的职责是当客户端向服务器端发送请求时,返回的请求结果。借助状态码,用户可以知道服务器端是正常处理了请求,还是出现了问题。状态码如200 OK,以3位数字和原因组成。

测试http协议格式:请求与响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#web应用程序:遵循http协议
import socket
sock=socket.socket()
sock.bind(("127.0.0.1",8080))
sock.listen(5)
while True:
'''
conn 表示新建立的套接字对象,用于在服务器和客户端之间进行数据传输。
addr 是一个元组,它包含了连接进来的客户端的 IP 地址和端口号。
'''
conn, addr = sock.accept()#阻塞等待客户端连接
data=conn.recv(1024)#请求报文
# data 是一个字节串,包含了客户端发送的请求信息。
print("客户端发送的请求信息:\n",data)
conn.send(b"HTTP/1.1 200 ok\r\nserver:zxj\r\n\r\nhello world")#响应首行+响应头+响应体
conn.close()

测试post请求:urlencoded格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import socket

# 创建 socket 连接
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 8080))

# 构造 POST 请求报文
path = "/" # 目标路径
headers = {
"Host": "127.0.0.1",
"Content-Type": "application/x-www-form-urlencoded", # 数据格式
"Content-Length": len("username=admin&password=123456") # 数据长度
}
body = "username=admin&password=123456" # 请求体(表单数据)

# 拼接请求报文
request = f"POST {path} HTTP/1.1\r\n"
for k, v in headers.items():
request += f"{k}: {v}\r\n"
request += "\r\n" # 空行分隔头部与主体
request += body

# 发送请求
client.send(request.encode('utf-8'))

# 接收响应
response = client.recv(4096)
print(response.decode('utf-8'))
client.close()

测试post请求:json格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import json

# 定义请求地址
url = "http://127.0.0.1:8080"
# 定义 JSON 数据(字典格式)
data = {
"username": "admin",
"password": "123456"
}

# 发送 POST 请求
response = requests.post(
url,
json=data # 使用 json 参数自动序列化字典并设置 Content-Type: application/json
)

# 输出响应结果
print("状态码:", response.status_code)
print("响应内容:", response.text) # 使用 text 获取原始响应文本
'''
客户端发送的请求信息:
b'POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nUser-Agent: python-requests/2.32.2\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nContent-Length: 43\r\nContent-Type: application/json\r\n\r\n{"username": "admin", "password": "123456"}'
'''

通过 json=data 参数,requests 会自动将字典转换为 JSON 字符串,并设置请求头 Content-Type: application/json,无需手动调用 json.dumps() 或配置 headers

SSL 验证是指通过 SSL 证书验证网站身份并确保通信安全的过程。其核心目标是确认服务器的真实性、防止身份伪造,并建立加密连接以保护数据传输的安全性

HTTPS(HyperText Transfer Protocol Secure)是以安全为目标的 HTTP 通道,通过在 HTTP 基础上加入加密和身份认证机制,确保数据传输的隐私性、完整性和服务器身份的真实性

https=http+ssl

通过 Content-Type,服务器可识别请求体(Body)的格式(如 JSON、表单数据),客户端可解析响应数据的类型(如 HTML、图片)

例如:conn.send(b”HTTP/1.1 200 ok:zxj*content-type:text/html**hello world<>“)

再例如:‘HTTP/1.1 200 ok:zxj*content-type:application/json**’

api接口

在开发web应用中,有两种应用模式:

1.前后端不分离:客户端看到的内容和所有页面效果都是有服务端提供出来的

4

2.前后端分离:把前端的页面效果(html,css,js分离到另一个服务端,python服务端只需要返回数据即可)

前端形成一个独立的网站,服务端构成一个独立的网站

5

应用程序编程接口(Application Programming Interface,API接口),就是应用程序对外提供了一个操作数据的入口,这个入口可以是一个函数或类方法,也可以是一个url地址或者一个网络地址。当客户端调用这个入口,应用程序则会执行对应代码操作,给客户端完成相对应的功能。

当然,api接口在工作中是比较常见的开发内容,有时候,我们会调用其他人编写的api接口,有时候,我们也需要提供api接口给其他人操作。由此就会带来一个问题,api接口往往都是一个函数、类方法、或者url或其他网络地址,不断是哪一种,当api接口编写过程中,我们都要考虑一个问题就是这个接口应该怎么编写?接口怎么写的更加容易维护和清晰,这就需要大家在调用或者编写api接口的时候要有一个明确的编写规范!!!

为了在团队内部形成共识,防止个人习惯差异引起的混乱,我们都需要找到一种大家都觉得很好的接口实现规范,而且这种规范能够让后端写的接口,用途一目了然,减少客户端和服务端双方之间的合作成本。

目前市面上大部分公司开发人员使用的接口实现规范主要有:restful、RPC。

REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移。它首次出现在2000年Roy Fielding的博士论文中。

RESTful是一种专门为Web开发而定义API接口的设计风格,尤其适用于前后端分离的应用模式中。

关键:面向资源开发

这种风格的理念认为后端开发任务就是提供数据的,对外提供的是数据资源的访问接口,所以在定义接口时,客户端访问的URL路径就表示这种要操作的数据资源。

对于数据资源分别使用POST、DELETE、GET、UPDATE等请求动作来表达对数据的增删查改

请求方法 请求地址 后端操作
POST /student/ 增加学生
GET /student/ 获取所有学生
GET /student/1 获取id为1的学生
PUT /student/1 修改id为1的学生
DELETE /student/1 删除id为1的学生

restful规范是一种通用的规范,不限制语言和开发框架的使用。事实上,我们可以使用任何一门语言,任何一个框架都可以实现符合restful规范的API接口。

fastapi快速开始

简单案例

安装:pip install fastapi

还需要一个ASGI服务器,生产环境使用Uvicorn:pip install uvicorn

ASGI(Asynchronous Server Gateway Interface )是一种异步服务器网关接口 ,为 Python Web 应用提供了标准接口,使其能够处理现代网络协议(如 WebSocket、HTTP/2 等)的异步请求。与传统的 WSGI 不同,ASGI 支持异步编程模型,允许单个请求处理多个事件(如长连接、双向通信),从而提升高并发场景下的性能

Uvicorn 是一个基于 ASGI 的高性能异步 Web 服务器,专为 Python 异步框架设计。

web应用程序=web框架+自己写的业务逻辑代码

1
2
3
4
5
6
7
8
9
10
11
12
from fastapi import FastAPI#fastapi类

app= FastAPI()#创建一个fastapi实例

@app.get("/")
async def home():

return {"user_id":1001}

@app.get("/shop")
async def shop():
return {"shop_id":1002}

启动:uvicorn "04 fastapi_begin:app" --reload

也可以:

1
2
3
import uvicorn
if __name__ == "__main__":
uvicorn.run(app="04 fastapi_begin:app",port=8080,reload=True)

接口文档

6

修饰器(Decorator)是 Python 中一种动态修改函数或类行为的高级功能,本质上是一个函数或类,它接受目标函数或类作为参数,并返回包装后的新函数或类对象,从而在不修改原始代码 的前提下为对象添加额外功能

路径操作

路径操作修饰器

1
2
3
4
5
6
7
8
@app.get()
@app.post()
@app.put()
@app.patch()
@app.delete()
@app.options()
@app.head()
@app.trace()

路径操作修饰器参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'''
tags为接口添加标签,用于在自动生成的文档
summary为接口添加描述
description为接口添加详细描述
response_description为接口返回值描述
deprecated为过时的接口
'''
@app.post("/post",
tags=["这是post方法"],
summary="这是post方法的描述",
description="这是post方法的详细描述",
response_description="这是post方法的返回值描述",
deprecated=True, # 过时的接口
)
def test():

include_router

文件路径如下

7

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from fastapi import FastAPI
import uvicorn

from apps.app01.shop import shop
from apps.app02.user import user

app= FastAPI()

app.include_router(shop,
prefix="/shop", # 路由前缀
tags=["购物中心接口"], # 标签
responses={200: {"description": "成功"}} # 响应描述
)
app.include_router(user,
prefix="/user", # 路由前缀
tags=["用户接口"], # 标签
responses={200: {"description": "成功"}} # 响应描述
)

if __name__ == "__main__":
uvicorn.run("main:app", port=8080, reload=True)

shop.py

1
2
3
4
5
6
7
8
9
10
11
from fastapi import APIRouter

shop=APIRouter()

@shop.get("/food")
def shop_food():
return {"food":"shop food"}

@shop.get("/drink")
def shop_drink():
return {"drink":"shop drink"}

user.py

1
2
3
4
5
6
7
8
9
10
11
from fastapi import APIRouter

user=APIRouter()

@user.post("/login")
def user_login():
return {"user":"user login"}

@user.post("/register")
def user_register():
return {"user":"user register"}

include_router 是 FastAPI 框架中用于整合路由的核心方法,其作用是将通过 APIRouter 定义的路由模块添加到主应用程序实例中,使这些路由在应用中生效。

8

请求与响应

4.1 路径参数

(1)基本用法

以使用与 Python 格式化字符串相同的语法来声明路径”参数”或”变量”:

1
2
3
4
@app.get("/user/{user_id}")
def get_user(user_id):
print(user_id, type(user_id))
return {"user_id": user_id}

路径参数 user_id 的值将作为参数 user_id 传递给你的函数。

(2)有类型的路径参数

你可以使用标准的 Python 类型标注为函数中的路径参数声明类型。

1
2
3
4
@app.get("/user/{user_id}")
def get_user(user_id: int):
print(user_id, type(user_id))
return {"user_id": user_id}

在这个例子中,user_id 被声明为 int 类型。

这将为你的函数提供编辑器支持,包括错误检查、代码补全等等。

(3)注意顺序

在创建路径操作时,你会发现有些情况下路径是固定的。

比如 /users/me,我们假设它用来获取关于当前用户的数据。

然后,你还可以使用路径 /user/{username} 来通过用户名获取关于特定用户的数据。

由于路径操作是按顺序依次运行的,你需要确保路径 /user/me 声明在路径 /user/{username} 之前。

如下

image-20250516193934740
image-20250516201128482

路由(Routing)是指在网络中选择数据传输路径的过程,其核心目标是将数据从源点高效、可靠地传输到目的地

cURL 是一个开源的命令行工具和跨平台的库(libcurl),用于基于 URL 语法在网络协议下进行数据传输。它支持多种协议(如 HTTP、HTTPS、FTP、SMTP 等),能够实现文件上传、下载以及与 Web 服务器的交互,常被开发者用于 API 测试、数据传输等场景

4.2 查询参数(请求参数)

路径函数中声明不属于路径参数的其他函数参数时,它们将被自动解释为查询字符串参数,就是 url?之后用 & 分割的 key-value 键值对

1
2
3
4
5
6
7
8
@app02.get("/jobs")
async def get_jobs(kind1: str, kind2: str, kind3: str):
#基于查询参数的值来执行不同的操作
return {
"kind1": kind1,
"kind2": kind2,
"kind3": kind3
}
image-20250516201707589

增加路径参数:kind1为路径参数

增加默认参数值

1
2
3
4
5
6
7
8
@app02.get("/jobs/{kind1}")
async def get_jobs(kind1: str, kind2: str="None", kind3: str="None"):#增加默认值,可选填
#基于查询参数的值来执行不同的操作
return {
"kind1": kind1,
"kind2": kind2,
"kind3": kind3
}

Request URL:

http://127.0.0.1:8080/app02/jobs/11?kind2=22&kind3=33

自python3.5开始,PEP484为python引入了类型注解(type hints),typing的主要作用有:

1.类型检查,防止运行时出现参数、返回值类型不符。

2.作为开发文档附加说明,方便使用者调用时传入和返回参数类型。

3.模块加入不会影响程序的运行不会报正式的错误,pycharm支持typing检查错误时会出现黄色警告。

type hints主要是要指示函数的输入和输出的数据类型,数据类型在typing包中,基本类型有str list dict等等,

Type Hints 是 Python 3.5 引入的功能,通过类型注解增强代码的可读性和可维护性。它允许开发者为变量、函数参数、返回值等指定预期的数据类型,从而帮助静态类型检查工具(如 mypy)捕获潜在错误,并提升 IDE 的智能提示能力。例如:

1
2
def greet(name: str) -> str:
return f"Hello, {name}"

此处 name: str 表示参数需为字符串类型,-> str 表示返回值类型为字符串 。

Union是当有多种可能的数据类型时使用,比如函数有可能根据不同情况有时返回str或返回list,那么就可以写成Union[list, str]

从 Python 3.10 起,Union[X, Y] 可简写为 X | Y。例如 int | str 等价于 Union[int, str]

再例如:kind2:str|None=None

Optional是Union的一个简化,当数据类型中有可能是None时,比如有可能是str也有可能是None,则Optional[str],相当于Union[str, None]

4.3 请求体数据

当你需要将数据从客户端(例如浏览器)发送给 API 时,你将其作为「请求体」发送。请求体是客户端发送给 API 的数据。响应体是 API 发送给客户端的数据。

FastAPI 基于 PydanticPydantic 主要用来做类型强制检查(校验数据)。不符合类型要求就会抛出异常。

对于 API 服务,支持类型检查非常有用,会让服务更加健壮,也会加快开发速度,因为开发者再也不用自己写一行一行的做类型检查。

安装上手 pip install pydantic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from fastapi import APIRouter
from pydantic import BaseModel,Field,field_validator
from datetime import date
from typing import Union,Optional

app03 = APIRouter()

class Address(BaseModel):
province: str
city: str

class User(BaseModel):
#正则表达式
#username:str=Field(pattern="^[a-zA-Z0-9]{3,10}$",title="用户名",description="用户名长度在3-10之间,且只能包含字母和数字")
#name: str|None = None
name:str
age: int=Field(default=0,gt=0,lt=100)
birth:Union[date,None] = None
friends:list[int]=[]
description:Optional[str]=None
#嵌套
addr:Address|None = None

'''
field_validator 的第一个参数必须是 cls,因为它是类方法 (classmethod),用于在验证字段时访问模型类的上下文。
'''
@field_validator("name")
def name_must_alpha(cls,value):
'''
value.isalpha() 会检查字符串是否只由字母组成,如果是则返回 True,否则返回 False。
如果返回 False,assert 触发,会抛出 AssertionError,并显示错误信息 "name must be alpha"。
'''
assert value.isalpha(), "name must be alpha"
return value

#嵌套
class Data(BaseModel):
data:list[User]

@app03.post("/user")
async def user(user:User):
print(user,type(user))
return user

@app03.post("/data")
async def data(data:Data):
print(data,type(data))
return data

BaseModel专门用于数据验证、数据转换和序列化。在定义数据结构时继承自 BaseModel,可以:

  • 自动校验数据类型:根据类中字段的类型注解,自动校验输入数据是否符合预期类型。
  • 数据转换:可以自动将输入数据(例如 JSON 字符串)转换成相应的 Python 数据类型。
  • 序列化输出:支持将模型实例转换成 JSON、字典等格式,便于响应输出。

在 Pydantic 中,Field 用于为模型字段提供额外的信息,比如设置默认值、描述信息、约束条件(例如长度、范围等)或别名。这可以帮助自动生成 OpenAPI 文档、增强验证或对字段进行更细粒度的控制。

field_validator 是 Pydantic v2 中用于替代旧版 @validator 的新装饰器,专门用于为模型字段添加自定义验证逻辑。它通过更清晰的命名和更灵活的模式(如 mode="before"mode="after")提升代码可读性和验证逻辑的控制能力

field_validatormodel_validator区别

field_validator专门针对单个字段 进行验证,适用于需要校验特定字段的规则(如长度、格式、类型约束)。例如验证用户名长度

model_validator作用于整个模型实例 ,适用于需要跨字段验证或全局逻辑的场景。例如检查两次密码是否一致

4.4 form表单数据

OAuth2 规范的一种使用方式(密码流)中,需要将用户名、密码作为表单字段发送,而不是 JSON。

FastAPI 可以使用 Form 组件来接收表单数据,需要先使用 pip install python-multipart 命令进行安装。

1
2
3
4
5
6
7
@app04.post("/register")
async def data(username:str=Form(),password:str=Form()):
print(f"username: {username}, password: {password}")
return {
"username": username,
"password": password
}

发送post请求:form表单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 目标 URL
url = "http://127.0.0.1:8080/app04/register"

# 表单数据(键值对)
data = {
"username": "test_user",
"password": "secure_password_123"
}

# 发送 POST 请求
response = requests.post(
url,
data=data
)

通过 requests.post() 的 data 参数传递表单数据,该参数接受字典或字符串格式的数据。requests 会自动将其编码为 application/x-www-form-urlencoded 格式

4.5 文件上传

导入必要库

1
2
3
from fastapi import File,UploadFile
import os
app05 = APIRouter()

通过字节上传

1
2
3
4
5
6
7
@app05.post("/file")
async def file(file: bytes = File(...)):
#适合小文件上传
return {
"filename": "file",
"size": len(file)
}

多文件上传

1
2
3
4
5
6
7
8
9
@app05.post("/files")
async def files(files: list[bytes] = File(...)):
#多文件上传
for file in files:
print(len(file))
return {
"filename": "files",
"size": len(files)
}

UploadFile上传,绝对路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app05.post("/uploadfile")
async def upload_file(file: UploadFile= File(...)):
#适合大文件上传
# 获取当前文件的绝对路径
base_dir = os.path.dirname(os.path.abspath(__file__))
print(base_dir)
img_dir = os.path.join(base_dir, "../imgs")
print(img_dir)
# 创建目录
path = os.path.join(img_dir, file.filename)
#path=os.path.join("../imgs",file.filename)
print(path)
with open(path, "wb") as f:
print("文件名:", file.filename)
for chunk in file.file:
f.write(chunk)

return {
"filename": file.filename
}

UploadFile是 FastAPI 提供的一个类,用于处理文件上传。与直接将文件内容读取为字节流(例如 bytes相比,UploadFile有以下优点:

  • 内存优化:它采用了文件对象的方式处理上传文件,不必将整个文件内容一次性加载到内存中,适合处理大文件。
  • 异步支持:支持异步操作,可以用异步方式读取文件内容,提高性能。
  • 文件元数据:提供文件名、内容类型等元数据信息,通过属性 filenamecontent_type 获取。
  • 文件接口:通过 file 属性获取一个类文件对象,可以像操作普通文件一样读取或保存上传的文件。

4.6 Request对象

有些情况下我们希望能直接访问 Request 对象。例如我们在路径操作函数中想获取客户端的 IP 地址,需要在函数中声明 Request 类型的参数,FastAPI 就会自动传递 Request 对象给这个参数,我们就可以获取到 Request 对象及其属性信息,例如 header、url、cookie、session 等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app06.post("/items")
async def items(request: Request):
print("url:", request.url)
print("客户端ip:", request.client.host)
print("请求头:", request.headers)
print("客户端宿主",request.headers.get("user-agent"))
print("cookie:", request.cookies)
return {
"url": request.url,
"client_ip": request.client.host,
"headers": request.headers,
"user_agent": request.headers.get("user-agent"),
"cookies": request.cookies
}

4.7请求静态文件

在 Web 开发中,需要请求很多静态资源文件(不是由服务器生成的文件),如 css/js 和图片文件等。

main.py

1
2
from fastapi.staticfiles import StaticFiles
app.mount("/static", StaticFiles(directory="statics"))#静态文件目录
image-20250517143857442
image-20250517143914772

静态网站

完全由静态文件(HTML、CSS、JavaScript)组成,内容固定不变,所有页面在开发时已预生成,无需动态计算或数据库支持

动态网站

内容根据用户请求实时生成,通常依赖数据库和服务器端编程(如PHP、Python、Node.js),能提供个性化和交互功能

StaticFiles是 FastAPI(实际来自 Starlette)提供的一个类,用于挂载和服务静态文件目录。 它的作用是让你可以通过 HTTP 路径直接访问服务器上的静态资源(如图片、CSS、JS 文件等)。

mount()方法用于将一个完整的应用或静态文件目录挂载到主 FastAPI 应用的某个路径下。这样,访问指定路径时,请求会被转发到挂载的应用或目录。

4.8 响应模型相关参数

response_model

response_model是 FastAPI 路由装饰器(如 @app.post@app.get 等)中的一个参数,用于指定接口响应的数据模型。它的作用是:

  • 自动校验和序列化:FastAPI 会根据你指定的 Pydantic 模型自动校验、过滤和格式化返回的数据。
  • 自动生成文档:接口文档会自动显示响应的数据结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str|None = None

class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str|None = None


@app07.post("/user02",response_model=UserOut)
async def create_user(user: UserIn):
# 这里可以进行一些处理,比如将用户信息存储到数据库中
return user

案例:

  • 注册功能
  • 输入账号、密码、昵称、邮箱,注册成功后返回个人信息
response_model_exclude_unset=True

通过上面的例子,我们学到了如何用 response_model 控制响应体结构,但是,如果它们实际上没有存储,则可能要从结果中忽略它们。例如,如果 model 在 NoSQL 数据库中具有很多可选属性,但是不想发送很长的 JSON 响应,其中包含默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Item(BaseModel):
name:str
description: str|None = None
price: float
tax:float=10.5
tags: list[str]|None = None

#模拟数据库
items={
"item01":Item(name="item01",price=10.5),
"item02":Item(name="item02",description="item02",price=20.5,tax=20.5,tags=["tag1","tag2"]),
"item03":Item(name="item03",description="item03",price=30.5,tax=30.5,tags=["tag1","tag2"]),
}

@app07.get("/items/{item_id}",response_model=Item,response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]

设置后返回为:

1
2
3
4
5
item01
{
"name": "item01",
"price": 10.5
}

当你设置 response_model_exclude_unset=True 时,返回的响应数据只包含被显式设置过的字段,没有被赋值的(即使用默认值且未传递的)字段不会出现在响应中。

其他参数

response_model_exclude_defaults 作用:排除所有值为默认值的字段。

response_model_exclude_none 作用:排除所有值为 None 的字段。

response_model_include 作用:只返回指定字段

response_model_exclude 作用:排除指定字段,不在响应中返回。

jinja2模板

要了解 jinja2,那么需要先理解模板的概念。模板在 Python 的 web 开发中广泛使用,它能够有效的将业务逻辑和页面逻辑分开,使代码可读性增强、并且更加容易理解和维护。

模板简单来说就是一个其中包涵占位变量表示动态的部分的文件,模板文件在经过动态赋值后,返回给用户。

jinja2 是 Flask 作者开发的一个模板系统,起初是仿 django 模板的一个模板引擎,为 Flask 提供模板支持,由于其灵活,快速和安全等优点被广泛使用。

在 jinja2 中,存在三种语法:

  1. 变量取值 {{ }}
  2. 控制结构 {% %}

应用于前后端不分离,模板html+数据库,返回动态网站

5.1 变量

main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from fastapi.templating import Jinja2Templates

templates=Jinja2Templates(directory="templates")

@app.get("/index")
def index(request: Request):
#数据库
name="World"
books=["Python", "Java", "C++"]
user={"name":"Tom", "age":18}

return templates.TemplateResponse(
"index.html",#模板文件
{
"request": request,# FastAPI需要一个request对象
"name": name,
"books": books,
"user": user
}#context上下文对象
)

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>

<body>
<h1>Hello, {{ name }}!</h1>
<p>Your favorite books are:</p>
<ul>
{% for book in books %}
<li>{{ book }}</li>
{% endfor %}
</ul>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
</body>

</html>
image-20250517200643492

5.2 过滤器

变量可以通过“过滤器”进行修改,过滤器可以理解为是 jinja2 里面的内置函数和字符串处理函数。常用的过滤器有:

过滤器名称 说明
capitalize 把值的首字母转换成大写,其他字母转换为小写
lower 把值转换成小写形式
title 把值中每个单词的首字母都转换成大写
trim 把值的首尾空格去掉
striptags 渲染之前把值中所有的 HTML 标签都删掉
join 拼接多个值为字符串
round 默认对数字进行四舍五入,也可以用参数进行控制
safe 渲染时值不转义

那么如何使用这些过滤器呢?只需要在变量后面使用管道 (|) 分割,多个过滤器可以链式调用,前一个过滤器的输出会作为后一个过滤器的输入。

例如:<h1>Hello, {{ name|upper }}!</h1> <li>{{ book|title }}</li>

5.3 控制结构

jinja2中的if语句类似与Python的if语句,它也具有单分支,多分支等多种结构,不同的是,条件语句不需要使用冒号结尾,而结束控制语句,需要使用endif关键字

1
2
3
4
5
6
7
8
9
10
11
12
<p>影视区</p>
{% if age >= 18 %}
<ul>
<li>成人影片</li>
<li>成人游戏</li>
</ul>
{% else %}
<ul>
<li>儿童影片</li>
<li>儿童游戏</li>
</ul>
{% endif %}

jinja2中的for循环用于迭代Python的数据类型,包括列表、元组和字典。在jinja2中不存在while循环。

1
2
3
{% for book in books %}
<li>{{ book|title }}</li>
{% endfor %}

ORM操作

在大型的 Web 开发中,我们肯定会用到数据库操作,那么 FastAPI 也支持数据库的开发,你可以用 PostgreSQL、MySQL、SQLite、Oracle 等。本文用 SQLite 为例。我们看下在 FastAPI 是如何操作设计数据库的。

FastAPI 是一个很优秀的框架,但是缺少一个合适的 ORM,官方代码里面使用的是 SQLAlchemy,Tortoise ORM 是受 Django 启发的易于使用的异步 ORM(对象关系映射器)。

Tortoise ORM 目前支持以下数据库:

  • PostgreSQL >= 9.4(使用 asyncpg)
  • SQLite(使用 aiosqlite)
  • MySQL/MariaDB(使用 aiomysql 或使用 asyncmy)

安装:pip install tortoise-orm

6.1 创建模型

1. 一对一关系(One-to-One)

  • 定义 :一张表中的一条记录仅关联另一张表中的一条记录。
  • 示例 :用户表(User)与身份证信息表(IDCard),一个用户仅对应一张身份证信息。

2. 一对多关系(One-to-Many)

  • 定义 :一张表中的一条记录关联另一张表中的多条记录。
  • 示例 :班级表(Class)与学生表(Student),一个班级包含多个学生,但每个学生只能属于一个班级。

3. 多对多关系(Many-to-Many)

  • 定义 :两张表中的记录可以互相关联多条记录,通常通过中间表实现。
  • 示例 :学生表(Student)与课程表(Course),一个学生可选修多门课程,一门课程也可被多个学生选修。

4. 自引用关系(Self-Referencing)

  • 定义 :表内的记录通过字段关联自身,形成层级或树状结构。
  • 示例 :员工表(Employee),每个员工可能有直属上级(另一个员工)。

5. 继承关系(Inheritance)

  • 定义 :基于面向对象的继承概念,子表继承父表的字段和约束。
  • 示例 :用户表(User)作为基表,管理员表(Admin)和普通用户表(RegularUser)继承其字段(如用户名、密码)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from tortoise.models import Model
from tortoise import fields
'''
Model 是所有数据模型的基类,通过继承 Model 可定义数据库表的结构。每个 Model 子类对应一张数据库表,其类属性定义了表的字段(列)及其约束。
fields 提供了多种字段类型,用于定义数据库表的列及其约束。
'''
class Student(Model):
id = fields.IntField(pk=True)#该字段会被指定为模型的主键
name = fields.CharField(max_length=50,description="姓名")
pwd = fields.CharField(max_length=50,description="密码")
sno = fields.IntField(description="学号")
#一对多关系
Class_id = fields.ForeignKeyField(
"models.Class",#关联的模型类
related_name="students",#反向查询时的名称
on_delete=fields.CASCADE,#级联删除
description="班级"
)
#多对多关系
Course_id = fields.ManyToManyField(
"models.Course",#关联的模型类
related_name="students",#反向查询时的名称
description="课程"
)

class Course(Model):
id = fields.IntField(pk=True)
name = fields.CharField(max_length=50,description="课程名称")
teacher = fields.CharField(max_length=50,description="授课老师")
#一对多关系
teacher_id = fields.ForeignKeyField(
"models.Teacher",#关联的模型类
related_name="courses",#反向查询时的名称
on_delete=fields.CASCADE,#级联删除
description="老师"
)

class Class(Model):
id = fields.IntField(pk=True)
name= fields.CharField(max_length=50,description="班级名称")

class Teacher(Model):
id =fields.IntField(pk=True)
name = fields.CharField(max_length=50,description="老师姓名")
pwd = fields.CharField(max_length=50,description="密码")
sno = fields.IntField(description="工号")

ORM(Object Relational Mapping,对象关系映射)是一种程序设计技术,主要用于实现面向对象编程语言关系型数据库 之间的数据转换。其核心思想是通过对象模型与数据库表结构的映射,将数据库操作转化为面向对象的操作,从而简化开发流程并提升代码的可维护性

Tortoise ORM 是一款专为 Python 异步环境设计的轻量级对象关系映射(ORM)框架,其设计灵感来源于 Django ORM,但专注于异步编程场景,适用于 FastAPI、Sanic 等基于 asyncio 的现代 Web 框架。

关系型数据库与非关系型数据库

关系型数据库 以表格形式存储数据,数据按行和列组织,列代表属性(字段),行代表记录。例如,用户表可能包含 idnameemail 等列,每行对应一个用户记录。这种结构化设计支持严格的模式约束(Schema)17。

典型代表 :MySQL、Oracle、PostgreSQL。

非关系型数据库(NoSQL) 采用非结构化或半结构化存储,常见的类型包括:

  • 文档型 (如 MongoDB):以 JSON 或 BSON 格式存储数据。

  • 键值型 (如 Redis):通过键直接访问值。

  • 列存储 (如 Cassandra):按列而非行组织数据。

  • 图数据库

    (如 Neo4j):以节点和边表示数据关系

6.2 aerich迁移工具

docker 安装 mysql

拉取 MySQL 镜像:docker pull mysql

运行 MySQL 容器:

1
docker run --name fastapi -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -d mysql

-p表示端口映射 –restart=always表示容器退出时总是重启 –name表示容器命名 –privileged=true表示赋予容器权限修改宿主文件权利 -v /home/mysql/log:/var/log/mysql表示容器日志挂载到宿主机 -v /home/mysql/data:/var/lib/mysql表示容器存储文件挂载到宿主机 -v /home/mysql/conf/my.cnf:/etc/mysql/my.cnf表示容器配置文件挂载到宿主机 -e MYSQL_ROOT_PASSWORD=a12bCd3_W45pUq6表示设置mysql的root用户密码,建议用强密码 -d表示后台运行

启动这个 MySQL 容器:docker start fastapi

进入 MySQL 容器:docker exec -it fastapi bash

这条命令的作用是:

  • docker exec:在已运行的 Docker 容器中执行命令。
  • -it-i 表示交互式操作,-t 分配一个伪终端(让你像在终端一样操作)。
  • fastapi:这是你要进入的容器名称(你的 MySQL 容器名)。
  • bash:在容器内启动 bash shell。

这条命令会让你进入名为 fastapi 的容器,并获得一个 bash 命令行界面,就像登录到一台 Linux 服务器一样,可以在里面执行各种命令(比如登录 MySQL、查看日志等)。

登录 MySQL:mysql -u root -p

从主机直接连接:mysql -h 127.0.0.1 -P 3306 -u root -p

配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
TORTOISE_ORM = {
"connections": {
"default": {
"engine":'tortoise.backends.mysql',#选择数据库引擎,mysql
"credentials": {
"host": "localhost",#数据库地址
"port": 3306,#数据库端口
"user": "root",#数据库用户名
"password": "root",#数据库密码
"database": "fastapi_db",#数据库名称
'charset': "utf8mb4",#数据库编码
'echo': True,#是否打印sql语句
'minsize': 1,#连接池最小连接数
'maxsize': 5#连接池最大连接数
}
}
},
"apps": {
"models": {
#db.models是我们自己定义的模型类,models在db文件夹下
"models": ["db.models","aerich.models"],
"default_connection": "default"
}
},
'use_tz': False,#是否使用时区
'timezone': 'Asia/Shanghai',#时区
"generate_schemas": True,
"add_exception_handlers": True
}

1.初始化配置,只需要使用一次

aerich 是一种 ORM 迁移工具,需要结合 tortoise 异步 orm 框架使用。安装 aerich

pip install aerich

aerich init -t settings.TORTOISE_ORM # TORTOISE_ORM 配置的位置

初始化完会在当前目录生成一个文件:pyproject.toml 和一个文件夹:migrations

  • pyproject.toml:保存配置文件路径,低版本可能是 aerich.ini
  • migrations:存放迁移文件
2.初始化数据库,一般情况下只用一次

aerich init-db

image-20250521105044582
3.更新模型并进行迁移

修改model类,重新生成迁移文件

aerich migrate

image-20250521111024194
4.重新执行迁移,写入数据库

aerich upgrade

image-20250521111131931
5.回到上一个版本

aerich downgrade

6.查看历史迁移记录

aerich history

register_tortoise 是 Tortoise ORM 提供的一个工具函数,用于在 FastAPI 等异步框架中快速集成和管理 Tortoise ORM 的生命周期(如启动时初始化数据库连接,关闭时释放资源)。其核心作用是简化 Tortoise ORM 的配置和自动化管理,开发者只需一行代码即可完成复杂的初始化流程

6.3 ORM查询操作

api.stud

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from fastapi import APIRouter
#导入数据库
from db.models import *

student_api=APIRouter()

@student_api.get("")
async def get_students():
"""
#查询所有学生信息
students= await Student.all()#获取所有学生信息
for student in students:
print(student.id,student.name)

#过滤查询filter
students= await Student.filter(name__contains="张").all()
for student in students:
print(student.id,student.name)

#get学生信息
stu = await Student.get(id=1)
print(stu.id,stu.name)

#模糊查询
#最大值
stu =await Student.filter(sno__gt=1000).all()
#最小值
#stu = await Student.filter(sno__lt=1000).all()
#范围查询
#stu = await Student.filter(sno__range=(1000,2000)).all()

#values查询
stu = await Student.all().values("sno","name")
"""

return{
"操作":"获取所有学生信息"
}

@student_api.post("")
async def create_student():
return{
"操作":"创建学生信息"
}

@student_api.get("/{student_id}")
async def get_student(student_id:int):
return{
"操作":f"获取学生信息,ID:{student_id}"
}

@student_api.put("/{student_id}")
async def update_student(student_id:int):
return{
"操作":f"更新学生信息,ID:{student_id}"
}

在 FastAPI 和 Tortoise ORM 中,asyncawait 用于异步编程,主要原因如下:

  1. 异步 I/O 操作 数据库查询(如 Student.all())是耗时的 I/O 操作。使用 async/await 可以在等待数据库响应时,不阻塞主线程,提高应用的并发性能。
  2. FastAPI 支持异步路由 FastAPI 支持异步(async def)的路由函数,这样可以充分利用 Python 的异步特性,提升 Web 服务的吞吐量。
  3. Tortoise ORM 的方法是异步的 Tortoise ORM 的数据库操作方法(如 .all().create() 等)本身就是异步方法,必须用 await 调用,并且所在函数必须用 async def 声明。

中间件

FastAPI 的「中间件(middleware)」就是 在请求进入路由函数之前、响应离开路由函数之后 插入的 通用处理逻辑

image-20250815144154925

什么时候需要中间件

真实场景举例 —— 统一鉴权 + 日志

需求 • 所有 API(/user、/order、/admin …)都必须验证 JWT; • 无论成功或失败,都记录一条结构化日志(URL、耗时、用户 ID、响应码); • 鉴权失败直接 401,不再进入任何路由。

实战

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from fastapi import FastAPI, Request
import uvicorn
app = FastAPI()

@app.middleware("http") # 注册一个全局中间件
async def mid1(request: Request, call_next):
print("mid1 request")
response = await call_next(request) #先让请求继续往后走
print("mid1 respones")
return response

@app.middleware("http")
async def mid2(request: Request, call_next):
print("mid2 request")
response = await call_next(request)
print("mid2 respones")
return response

@app.get("/") # 任意路由
def home():
print("home")
return {"msg": "ok"}


if __name__ == "__main__":
uvicorn.run(app, host="127.0.0.1", port=8000)

注意中间件添加的顺序

FastAPI/Starlette 把后添加的中间件包在最外层(洋葱最外层)

所以 mid2 在外层,请求先打印

1
2
3
4
5
mid2 request
mid1 request
home
mid1 response
mid2 response

cors组件

CORS(Cross-Origin Resource Sharing,跨源资源共享) 一句话:浏览器为了安全,默认禁止网页去“别的域名/端口/协议”拿数据;CORS 是一套 HTTP 机制,让服务器告诉浏览器“我允许谁来拿、拿什么、怎么拿”。

同源与跨域

同源(Same-Origin)

浏览器把下面三个部分合称 “源”(origin):

  1. 协议(http: / https:
  2. 域名(example.com / sub.example.com 算不同)
  3. 端口(:80 / :8080

只有当 协议 + 域名 + 端口 都完全一致时,才叫同源。 此时前端 JS 可以无限制地访问该源下的资源。

跨域(Cross-Origin)

只要 协议、域名、端口 中的任意一个不同,就是跨域

正常来说,服务器为了保护数据,会拒绝跨域的响应,但是通过cors可以允许跨域,因此,CORS 不是“绕过”安全限制,而是服务器主动声明的“安全白名单”。

1
2
3
4
5
6
7
8
9
# 为 FastAPI 应用添加 CORS(跨源资源共享)中间件
# 允许浏览器跨域访问本服务,开发阶段常用;生产环境请收窄范围
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 允许所有来源(生产建议改成具体域名列表,如 ["https://foo.com"])
allow_credentials=False, # 是否允许携带 Cookie/Authorization;True 时 origins 不能为 "*"
allow_methods=["GET"], # 允许的 HTTP 方法;["*"] 表示全部
allow_headers=["*"], # 允许的自定义请求头;["*"] 表示全部
)

知识点

端口查询

查看所有端口占用情况:netstat -ano

查询特定端口是否被占用:netstat -ano | findstr 8080

使用 taskkill 命令强制结束进程:taskkill /PID 进程ID /F

通过 PID 查找进程:tasklist | findstr PID

获取绝对路径

获取当前文件的绝对路径:base_dir = os.path.dirname(os.path.abspath(__file__))

拼接路径:img_dir = os.path.join(base_dir, "../imgs")

mysql部分指令

查看数据库列表SHOW DATABASES;

选择数据库USE 数据库名;

删除数据库DROP DATABASE 数据库名;

创建数据库CREATE DATABASE 数据库名;

登录 MySQLmysql -u root -p

退出exit

docker部分指令

linux安装dockersudo apt-get update && sudo apt-get install docker.io

查看 Docker 版本信息docker version

查看镜像docker images

查看所有的容器docker ps -a

systemctlsystemd 系统和服务管理器的核心工具,用于管理系统和服务的状态及配置。

mysql-client 是 MySQL 数据库的命令行客户端工具。它允许你通过命令行连接和操作 MySQL 数据库服务器,比如执行 SQL 查询、管理数据库和用户等。

常用命令格式如下:mysql -h 主机地址 -P 端口号 -u 用户名 -p

你可以在终端输入以下命令来检查是否已安装 mysql-clientmysql --version

可以使用以下命令安装:sudo apt-get update sudo apt-get install mysql-client

sudo apt-get update 这个命令的作用是更新本地软件包列表

停止并删除容器docker stop fastapi docker rm fastapi

参考资料

fastapi一个项目FastAPI进阶_哔哩哔哩_bilibili

教程fastapi框架快速学习_哔哩哔哩_bilibili

fastapi相关知识的补充Python 异步编程 - 搞明白 async, await (继续解释 yield)_哔哩哔哩_bilibili

复习:在前面我们已经学习了Pandas基础,第二章我们开始进入数据分析的业务部分,在第二章第一节的内容中,我们学习了数据的清洗,这一部分十分重要,只有数据变得相对干净,我们之后对数据的分析才可以更有力。而这一节,我们要做的是数据重构,数据重构依旧属于数据理解(准备)的范围。

开始之前,导入numpy、pandas包和数据

1
2
3
# 导入基本库
import numpy as np
import pandas as pd
1
2
3
# 载入上一个任务人保存的文件中:result.csv,并查看这个文件
text = pd.read_csv('result.csv')
text.head()
Unnamed: 0 PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th… female 38.0 1 0 PC 17599 71.2833 C85 C
2 2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S

2 第二章:数据重构

第一部分:数据聚合与运算

2.6 数据运用

2.6.1 任务一:通过教材《Python for Data Analysis》P303、Google or anything来学习了解GroupBy机制

1
2
3
4
5
6
7
8
#写入心得
'''
GroupBy机制是Pandas中用于数据分组与聚合的核心操作,其本质是遵循"Split-Apply-Combine"模式:

Split:按指定键(列、函数、数组等)将数据分割成多个子集
Apply:对每个子集独立应用聚合函数(如mean/max)、转换函数(如标准化)或过滤操作
Combine:将结果合并为新的数据结构
'''

2.4.2:任务二:计算泰坦尼克号男性与女性的平均票价

1
2
3
4
5
# 写入代码
df = text['Fare'].groupby(text['Sex'])
print(df.groups)
means = df.mean()
means
{'female': [1, 2, 3, 8, 9, 10, 11, 14, 15, 18, 19, 22, 24, 25, 28, 31, 32, 38, 39, 40, 41, 43, 44, 47, 49, 52, 53, 56, 58, 61, 66, 68, 71, 79, 82, 84, 85, 88, 98, 100, 106, 109, 111, 113, 114, 119, 123, 128, 132, 133, 136, 140, 141, 142, 147, 151, 156, 161, 166, 167, 172, 177, 180, 184, 186, 190, 192, 194, 195, 198, 199, 205, 208, 211, 215, 216, 218, 229, 230, 233, 235, 237, 240, 241, 246, 247, 251, 254, 255, 256, 257, 258, 259, 264, 268, 269, 272, 274, 275, 276, ...], 'male': [0, 4, 5, 6, 7, 12, 13, 16, 17, 20, 21, 23, 26, 27, 29, 30, 33, 34, 35, 36, 37, 42, 45, 46, 48, 50, 51, 54, 55, 57, 59, 60, 62, 63, 64, 65, 67, 69, 70, 72, 73, 74, 75, 76, 77, 78, 80, 81, 83, 86, 87, 89, 90, 91, 92, 93, 94, 95, 96, 97, 99, 101, 102, 103, 104, 105, 107, 108, 110, 112, 115, 116, 117, 118, 120, 121, 122, 124, 125, 126, 127, 129, 130, 131, 134, 135, 137, 138, 139, 143, 144, 145, 146, 148, 149, 150, 152, 153, 154, 155, ...]}

Sex
female    44.479818
male      25.523893
Name: Fare, dtype: float64

在了解GroupBy机制之后,运用这个机制完成一系列的操作,来达到我们的目的。

下面通过几个任务来熟悉GroupBy机制。

2.4.3:任务三:统计泰坦尼克号中男女的存活人数

1
2
3
# 写入代码
survived_sex = text['Survived'].groupby(text['Sex']).sum()
survived_sex.head()
Sex
female    233
male      109
Name: Survived, dtype: int64

2.4.4:任务四:计算客舱不同等级的存活人数

1
2
3
# 写入代码
survived_pclass = text['Survived'].groupby(text['Pclass'])
survived_pclass.sum()
Pclass
1    136
2     87
3    119
Name: Survived, dtype: int64

提示:】表中的存活那一栏,可以发现如果还活着记为1,死亡记为0

思考】从数据分析的角度,上面的统计结果可以得出那些结论

【思考】从任务二到任务三中,这些运算可以通过agg()函数来同时计算。并且可以使用rename函数修改列名。你可以按照提示写出这个过程吗?

1
2
3
4
5
6
7
#思考心得
'''
agg() 函数是 Pandas 中用于对分组后的数据 同时执行多个聚合操作 的核心工具,其全称为 Aggregate(聚合)。它允许你对不同列应用不同的聚合函数,并支持自定义函数,极大提升数据分析效率。
'''

text.groupby('Sex').agg({'Fare': 'mean', 'Pclass': 'count'}).rename(columns=
{'Fare': 'mean_fare', 'Pclass': 'count_pclass'})
mean_fare count_pclass
Sex
female 44.479818 314
male 25.523893 577

2.4.5:任务五:统计在不同等级的票中的不同年龄的船票花费的平均值

1
2
3
4
# 写入代码
temp=text.groupby(['Pclass','Age'])['Fare']
temp.groups
temp.mean().head()
Pclass  Age  
1       0.92     151.5500
        2.00     151.5500
        4.00      81.8583
        11.00    120.0000
        14.00    120.0000
Name: Fare, dtype: float64

2.4.6:任务六:将任务二和任务三的数据合并,并保存到sex_fare_survived.csv

1
2
3
4
# 写入代码
result = pd.merge(means,survived_sex,on='Sex')
result
result.to_csv('sex_fare_survived.csv')

2.4.7:任务七:得出不同年龄的总的存活人数,然后找出存活人数最多的年龄段,最后计算存活人数最高的存活率(存活人数/总人数)

1
2
3
4
# 写入代码
survived_age = text['Survived'].groupby(text['Age']).sum()
survived_age.head()

Age
0.42    1
0.67    1
0.75    2
0.83    2
0.92    1
Name: Survived, dtype: int64
1
2
# 写入代码
survived_age[survived_age.values==survived_age.max()]
Age
24.0    15
Name: Survived, dtype: int64
1
2
_sum = text['Survived'].sum()
print(_sum)
342
1
2
3
4
5
print("sum of person:"+str(_sum))

precetn =survived_age.max()/_sum

print("最大存活率:"+str(precetn))
sum of person:342
最大存活率:0.043859649122807015

我想听他扫弦的声音

南京1701livehouse

1745077840515
1745077840490
1745077840469
1745077840480

前言

我是一个很珍惜回忆的人,但是任何回忆都有忘却的那一天,所以我能做的就是尽可能把他ji’lu

1912

2025年的生日,与李哥在百家湖1912聚餐,李哥请我吃的铁板烧,超级美味

1745080784805
1745080784820
1745080784834
1745080784848
1745080784862
1745080784789
0%