Contents

数据科学

参考资料

Data 100 Fall 2022 website 公开课

Data 100

lec 01

  • 读取压缩文件内容

    1
    2
    3
    4
    5
    6
    
    with zipfile.ZipFile(filename, "r") as zf:
        # 列出压缩包内的文件
        print(zf.filelist)
        f = '想要打开的文件名'
        with zf.open(f) as fp:
            ......
    
  • 透视表

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    (
        pd.pivot_table(
            babynames[babynames['Name'].isin(subset_names)],
            index=['Name', 'Year'], columns='Sex', values='Count',
            aggfunc='sum', fill_value=0, margins=True
        )
        .drop(labels='All', level=0, axis=0) # 删除汇总行
        .rename_axis(None, axis=1) # 去掉列索引名
        # .reset_index() # 把 (Name, Year) 从索引挪回列
    )
    

    如上, 对于过长的链式代码可以用括号包裹并分行书写, 提高可读性

  • 判断值是否在某个列表中

    1
    
    pd.Series.isin([])
    
  • 添加一新列 col

    1
    2
    3
    4
    
    .assign(col=lambda row: ...)
    
    # 例如:
    .assign(Propf=lambda row: row.F / (row.F + row.M))
    
  • 好用的筛选方法

    1
    
    .query("@propf_min < Propf < @propf_max & Year > @year_thresh & Name != 'All'")
    

    其中 @变量名 表示引用在查询上下文中 已定义的变量

lec 02

  • 判断值是否以某字符开头

    1
    
    elections["Party"].str.startswith("A")
    
  • 字段值去重

    1
    
    elections["Party"].unique()
    
  • 对字段值进行排序

    1
    2
    3
    
    elections["Candidate"].sort_values(ascending = )
    # 或者
    elections.sort_values("Candidate", ascending = )
    

lec 03

有时候 DataFrame 不在单元格的最后一行, 可以使用 display, 优化展示结构

1
display(mydf)
  • 按照某一列分组, 然后取出 每一组的第一个数据

    1
    
    .groupby(colname).first()
    

    例如

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    import pandas as pd
    
    a = pd.DataFrame(
        data={
            'col1': ['a', 'a', 'a', 'b', 'c'],
            'col2': [133, 20, 30, 22, 33],
        }
    )
    display(a)
    a.groupby('col1').first()
    

lec 04

  • 设置输出格式, 例如行列数、精度等等

    1
    2
    3
    4
    5
    6
    
    np.set_printoptions(threshold=20, precision=2, suppress=True)
    
    pd.set_option('display.max_rows', 30)
    pd.set_option('display.max_columns', None)
    pd.set_option('display.precision', 2)
    pd.set_option('display.float_format', '{:.2f}'.format)
    

lec 05

  • 将某一列转换成字符串, 再进行文本替换

    1
    
    Series.str.replace
    
  • 根据参数 col 指定某一个特征类别绘制多个图像, 并放在一起便于对比

    1
    
    sns.catplot()
    
  • 对列名进行重命名, 注意需要传入参数名 columns

    1
    2
    3
    4
    
    tmp = {0: "Day", 1: "Month", 2: "Year", 3: "Hour", 4: "Minute", 5: "Second", 6: "Time Zone"}
    b = b.rename(columns=tmp)
    # 或者
    b.columns = ['...', '...', '...']
    

宽表转长表

1
pd.melt()

one-hot 是将分类变量的每一个可能取值,都变成一个独立的 0/1 特征列, 即宽表
列数可能随属性值的增加而爆炸

pd.melt 更准确的语义是“把宽格式转为长格式”

  • 宽表(Wide Format)例子

    各城市的年度销售额

    城市 2023 2024 2025
    北京 100 120 140
    上海 150 170 190
  • 长表(Long Format)例子

    城市 年份 销售额
    北京 2023 100
    北京 2024 120
    北京 2025 140
    上海 2023 150
    上海 2024 170
    上海 2025 190

    行数变多,重复信息多

  • 转换

    • 宽 → 长(熔接 / melt)

      把多列“压扁”成一列变量名和一列变量值,其余标识列重复
      关键操作:指定哪些列是“标识符”(不变),哪些列是“被熔化的列”(变成变量+值)

    • 长 → 宽(透视 / pivot)

      把变量列中的不同取值展开成多列,值列填充到对应的单元格
      关键操作:指定索引列、列名列、值列

可以把宽表看作“二维矩阵”,长表看作“三元组列表”

lec 06

  • 找到一组数的分位数值

    1
    2
    3
    4
    5
    6
    
    import numpy as np
    
    
    np.percentile(a, q)
    # q 是 0~100 之间的浮点数,表示百分位数
    # 用法解释:https://blog.csdn.net/qq_16488989/article/details/116295686
    
  • 熟悉常用的绘图函数

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    import seaborn as sns
    
    
    sns.countplot()
    sns.histplot()
    sns.rugplot()
    sns.boxplot()
    sns.violinplot()
    sns.scatterplot()
    sns.jointplot()
    sns.lmplot()
    

lec 07

  • DataFrame 替换

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    df.replace({
        'educ': {
            1: "<HS", 2: "HS", 3: "<BA", 4: "BA", 5: ">BA"
        }
    })
    
    # 或者
    df['educ'].replace(
        [1, 2, 3, 4, 5],
        ["<HS", "HS", "<BA", "BA", ">BA"]
    )
    
  • 设置某列为索引并按照某列分组

    1
    2
    3
    4
    5
    
    b = a.set_index("???").groupby("Gender")
    
    b.get_group("Men").drop("Gender", axis=1)
    
    b.get_group("Women").drop("Gender", axis="columns")
    
  • 绘制曲线时, 可以循环使用曲线类型和颜色

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    import matplotlib.pyplot as plt
    from cycler import cycler
    
    
    linestyles = ['-', '--', ':', '-.' ]
    colors = plt.cm.Dark2.colors
    
    lines_c = cycler('linestyle', linestyles)
    color_c = cycler('color', colors)
    
    ax.set_prop_cycle(color_c * lines_c)
    
  • 若条件 cond <= a/2 则返回 1/a,否则返回 0

    1
    2
    3
    4
    5
    
    np.piecewise(
        x,
        [cond <= a/2, cond > a/2],
        [1/a, 0]
    )
    

    其中 cond 通常是关于 x 的表达式

lec 08

  • 设置 sns 绘图格式

    1
    2
    3
    4
    5
    6
    7
    
    sns.set_theme(
        style = 'darkgrid',
        font_scale = 1.5,
        rc = {
            'figure.figsize':(7,5)
        }
    )
    
  • 先分组聚合再重置索引

    1
    2
    3
    
    ???.groupby(
        ["?", "??"]
    ).agg("mean").reset_index()
    
  • 随机采样中的 replace 参数

    1
    
    .sample(1000, replace = False)
    

    replace = False 无放回抽取
    replace = True 有放回抽取

lec 11

  • DataFrame 转换成 Numpy 数组

    1
    
    X = X.to_numpy()
    
  • R 方计算公式、相关系数计算公式

    在简单线性回归中,R 方(决定系数)与相关系数 r 的关系为:R² = r²
    其中 r 是 y 与 x 之间的皮尔逊相关系数

lec 16

  • 在图中添加子图

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    fig, ax = plt.subplots()
    
    ax.plot(
        "fraction_date",
        "data_filled",
        "r.",
        data=data
    )
    
    # 子图
    axin1 = ax.inset_axes([0.1, 0.5, 0.35, 0.35])
    axin1.plot(data2, "b")
    
  • 获取当前文件的绝对路径

    1
    2
    3
    4
    5
    6
    
    from pathlib import Path
    
    
    myfile = './lec16.ipynb'
    DATA_DIR = Path(myfile).absolute()
    DATA_DIR
    
  • 获取 home 目录路径再拼接其他路径

    1
    
    Path.home().joinpath('python', 'scripts', 'test.py')
    
  • 读取 xarray 数据文件

    1
    2
    3
    4
    
    import xarray as xr
    
    
    ds = xr.open_dataset('xarray文件名')
    
  • 绘制俯视地球仪视角的图像

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    import cartopy.crs as ccrs
    
    
    pfit = data['...'].polyfit("time", 1)
    p = pfit.polyfit_coefficients[0].plot(
        subplot_kws=dict(projection=ccrs.Orthographic(0, 90), facecolor="gray"),
        transform=ccrs.PlateCarree(central_longitude=0),
        cbar_kwargs={"label": "trend deg/yr"},
    )
    
    p.axes.coastlines();
    

lec 17

直方图 hist
条形图、柱状图 barh

通过 随机抽样 来模拟 数据的分布 情况

lec 19

  • 绘制散点图并进行拟合

    1
    
    sns.lmplot(data = )
    

lec 20

  • 将筛选出的内容保存到变量 var 中

    1
    2
    3
    4
    
    %%sql var <<
    这里是一些MySQL的筛选语句。。。
    
    display(var)
    
  • 执行筛选语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    query = """
    SQL 的筛选语句
    """
    
    %%sql
    {query}
    # 或者
    %%sql
    $query
    
  • 缩短 IPython 的错误显示

    1
    
    %xmode minimal
    

lec 21

1
2
3
e = 'hello'
print(f'e = {e}')
print(f'{e = }')    # 会输出 e = 'hello'
  • 一种文件路径写法, 注意斜杠后面加字符串

    1
    2
    3
    4
    5
    6
    
    from pathlib import Path
    
    
    df = pd.read_csv(
        Path.home() / "shared/climate-data/monthly_in_situ_co2_mlo_cleaned.csv"
    )
    
  • 缩写解释

    • HDF5:一种灵活的二进制文件格式,可存储分层嵌套的数据,并支持任意数值类型的多维密集数组
    • NetCDF:一个数据模型,用于以维度、变量和属性的形式组织数组
      • Xarray:一个用于数值计算和数据分析的 Python 库,它将 NetCDF 数据模型暴露为 Python 对象

lec 22

做奇异值分解需要先做数据中心化, 即对于每一列, 我们应该减去该列的平均值

  • 分组后查看每个组的数量大小

    1
    2
    3
    
    df.groupby(
        ['member', 'vote']
    ).size().to_frame()
    
  • 旋转坐标轴, rotation 控制角度

    1
    
    plt.xticks(..., rotation=90);
    
  • 对数据进行采样, 返回迭代器

    需要通过 for 循环来赋值

    1
    2
    3
    
    # 随机取出 20 个散点
    for _, row in mydata.sample(20).iterrows():
        a, b = row['...'], row['...']
    

lec 23

  • 生成一个含有 longitude、latitude、geometry 字段的 DataFrame

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    import geopandas as gpd
    
    
    gpd.GeoDataFrame(
        df,
        geometry=gpd.points_from_xy(
            df.longitude, df.latitude
        )
    )
    

lec 24

  • 绘制有 抖动 的图像, 防止散点重叠

    1
    
    sns.stripplot
    
  • 数据分箱 操作

    1
    2
    3
    4
    5
    
    bins = pd.cut()
    
    bins[0].left
    bins[0].right
    bins[0]
    
  • 适应子图之间的填充距离

    1
    
    plt.tight_layout()
    
  • 优化函数

    1
    
    from scipy.optimize import minimize
    
  • 单次观测的交叉熵损失方程

    $$ \textrm{loss} = -y \log(\hat{y}) - (1-y)\log(1-\hat{y}) $$

  • 逻辑函数

    $\sigma(t) = \frac{1}{1 + e^{-t}}$, 其中 $t = \theta_{0}+\theta_{1}x$

    • $\theta_{1}$ 绝对值越大, 越不平滑, 即越靠近点 (0,1) 和点 (0,0)
    • $\theta_{1}$ 小于 0 形状为 2, 大于 0 形状为 S
  • $这里 ~~~~~ 到这里有空格$

    数学公式中的空格在 LaTeX 中使用 ~ 来插入不可见空格

lec 25

字符串前加 r,表示原始字符串,避免反斜杠被转义
相当于双反斜杠的效果

  • 展示原始的数据离散分布情况

    用于绘制出 一维数组数据点实际的分布位置 情况, 单纯地将记录值在坐标轴上表现出来

    1
    
    sns.rugplot();
    
  • 去掉图例的标题

    1
    
    plt.gca().legend_.set_title(None)
    
  • 上下翻转 y 轴

    1
    
    plt.gca().invert_yaxis()
    

关于 SVD

简介

Singular Value Decomposition(奇异值分解) 是一种提取信息的强大工具。
它将矩阵分解为三个更简单的矩阵,能够揭示数据中隐藏的潜在模式。

主要应用领域:

  • 隐性语义分析 (Latent Semantic Analysis, LSA) 或隐性语义索引 (Latent Semantic Indexing, LSI)
  • 推荐系统 (Recommender system)
  • 矩阵形式数据(主要是图像数据)的压缩

线性变换

在推导 SVD 之前,先回顾一下线性变换。以 $2\times2$ 对角矩阵为例:

$$ M = \begin{bmatrix}3 & 0 \ 0 & 1\end{bmatrix} $$

从集合上讲,M 是将二维平面上的点 (x,y) 经过线性变换到另一个点的 变换矩阵,如下所示:

$$ \begin{bmatrix}3&0\0&1\end{bmatrix}\begin{bmatrix}x \ y\end{bmatrix} = \begin{bmatrix}3x \ y\end{bmatrix} $$

该变换的几何效果是,变换后的平面沿 $x$ 轴方向拉伸 3 倍,$y$ 轴方向不变。

SVD 推导

核心思想:对于任意矩阵 $M$,我们寻找一组原始空间的标准正交基 $\mathbf{v}_i$
使得它们经过 $M$ 变换后得到的向量 $M\mathbf{v}_i$ 仍然两两正交(只是长度可能改变)

假设找到了这样一组正交单位向量 $\mathbf{v}_1, \mathbf{v}_2$(即右奇异向量),使得 $M\mathbf{v}_1$ 与 $M\mathbf{v}_2$ 正交。
记 $\mathbf{u}_1, \mathbf{u}_2$ 分别为它们方向上的单位向量,长度为 $\sigma_1, \sigma_2$:

$$ M\mathbf{v}_1 = \sigma_1 \mathbf{u}_1,\qquad M\mathbf{v}_2 = \sigma_2 \mathbf{u}_2 $$

这里的 $\sigma_1, \sigma_2$ 称为矩阵 $M$ 的奇异值,表示原向量在变换中的拉伸倍数。

任意向量 $\mathbf{x}$ 可用这组正交基展开:

$$ \mathbf{x} = (\mathbf{v}_1\cdot \mathbf{x})\mathbf{v}_1 + (\mathbf{v}_2\cdot \mathbf{x})\mathbf{v}_2 $$

例如,仍取上面 $M=\begin{bmatrix}3&0\0&1\end{bmatrix}$,此时很容易看出 $\mathbf{v}_1=[1,0]^T, \mathbf{v}_2=[0,1]^T$ 就是一组合适的正交基(变换后方向不变,依然正交)。
若 $\mathbf{x}=[3,2]^T$,则 $\mathbf{x} = 3\mathbf{v}_1 + 2\mathbf{v}_2$,于是:

$$ M\mathbf{x} = 3 M\mathbf{v}_1 + 2 M\mathbf{v}_2 = 3\cdot(3\mathbf{u}_1) + 2\cdot(1\mathbf{u}_2)=9\mathbf{u}_1+2\mathbf{u}_2 $$

与直接计算 $M\mathbf{x} = [9,2]^T$ 一致。

回到一般推导,利用线性性质:

$$ M\mathbf{x} = (\mathbf{v}_1\cdot \mathbf{x})M\mathbf{v}_1 + (\mathbf{v}_2\cdot \mathbf{x})M\mathbf{v}_2 $$

$$ M\mathbf{x} = (\mathbf{v}_1^T\mathbf{x})\sigma_1\mathbf{u}_1 + (\mathbf{v}_2^T\mathbf{x})\sigma_2\mathbf{u}_2 $$

将标量 $\mathbf{v}_i^T\mathbf{x}$ 移到后方:

$$ M\mathbf{x} = \mathbf{u}_1\sigma_1(\mathbf{v}_1^T\mathbf{x}) + \mathbf{u}_2\sigma_2(\mathbf{v}_2^T\mathbf{x}) = (\mathbf{u}_1\sigma_1\mathbf{v}_1^T + \mathbf{u}_2\sigma_2\mathbf{v}_2^T),\mathbf{x} $$

两边去掉 $\mathbf{x}$,得到:

$$ M = \mathbf{u}_1\sigma_1\mathbf{v}_1^T + \mathbf{u}_2\sigma_2\mathbf{v}_2^T $$

推广到一般情况,将向量拼成矩阵:

$$ M = U\Sigma V^T $$

其中:

  • $U$ 的列是 $\mathbf{u}_1, \mathbf{u}_2, \dots$(左奇异向量,变换后的新正交基);
  • $V$ 的列是 $\mathbf{v}_1, \mathbf{v}_2, \dots$(右奇异向量,原始正交基);
  • $\Sigma$ 是对角矩阵,对角线元素为奇异值 $\sigma_1 \ge \sigma_2 \ge \dots \ge 0$。

总结

  • 任意矩阵 $M$ 都可分解为 $U\Sigma V^T$。
  • $V$ 的列是原始空间的标准正交基,$U$ 的列是变换后的标准正交基。
  • $\Sigma$ 的对角元素(奇异值)表示对应方向上拉伸/压缩的比例,且按从大到小排列,值越大代表该维度越重要。

用 SVD 做信息压缩:只保留前 $k$ 个最大的奇异值(及对应的奇异向量),即可用较少的维度近似原始矩阵。常用启发式策略:

  • 直接指定保留前 $k$ 个奇异值;

  • 保留一定百分比的“能量”(例如 90%),先计算所有奇异值的平方和,然后依次累加直到占比超过阈值:

    $$ k = \min_k\ \left{ \frac{\sum_{i=1}^{k}\sigma_i^2}{\sum_{i=1}^{r}\sigma_i^2} \ge 0.9 \right} $$

    其中 $r$ 为矩阵的秩(非零奇异值的个数)。

代码示例

 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
import numpy as np
import matplotlib.pyplot as plt
from skimage import io

def getImgAsMatFromFile(filename):
    img = io.imread(filename, as_gray=True)
    return np.array(img)

def plotImg(imgMat):
    plt.imshow(imgMat, cmap='gray')
    plt.show()

def recoverBySVD(imgMat, k):
    # 奇异值分解,注意返回的 Vh 是 V 的转置
    U, s, Vh = np.linalg.svd(imgMat, full_matrices=False)
    # 选取前 k 个最重要的奇异值
    Uk = U[:, :k]
    Sk = np.diag(s[:k])
    Vk = Vh[:k, :]
    # 重建图像
    imgMat_new = Uk @ Sk @ Vk
    return imgMat_new

# 示例:使用时请替换为真实图像路径
A = getImgAsMatFromFile('example.jpg')
plotImg(A)
A_new = recoverBySVD(A, 30)
plotImg(A_new)