跳转至

Python 数据处理进阶:Pandas / Polars 生信实战教程

一句话说明

Pandas 是 Python 最常用的表格数据处理库,Polars 是它的高性能替代;掌握这两个工具,你就能用 Python 完成 R 里 dplyr 能做的一切数据清洗和统计汇总工作。


为什么要学

场景 没有 Pandas 有 Pandas
合并 200 个样本的 Kraken2 丰度表 手写循环逐行解析 TSV,容易出错 pd.concat() 一行搞定
按分组(健康 vs 糖尿病)算平均丰度 嵌套字典 + for 循环 df.groupby("group").mean()
找缺失值并填充 自己写 if/else 判断 df.fillna(0)
处理百万行大文件 内存爆掉 chunksize 分块读取

面试高频点:生信岗位笔试/机试常考 Pandas 操作(groupby、merge、apply),手写能力很重要。


高频操作详解

1. 读写 CSV / Excel / TSV

import pandas as pd

# === 读取 CSV(逗号分隔)===
# 最基础的读法,第一行自动当列名
df_csv = pd.read_csv("sample_metadata.csv")

# === 读取 TSV(制表符分隔,生信最常见)===
# Kraken2、MetaPhlAn 输出都是 TSV
df_tsv = pd.read_csv("species_abundance.tsv", sep="\t")

# === 读取 Excel ===
# 需要安装 openpyxl:pip install openpyxl
df_excel = pd.read_excel("metadata.xlsx", sheet_name="Sheet1")

# === 只读前 100 行(快速预览大文件)===
df_peek = pd.read_csv("huge_file.tsv", sep="\t", nrows=100)

# === 指定列的数据类型(节省内存)===
df_typed = pd.read_csv("abundance.tsv", sep="\t", dtype={
    "sample_id": str,        # 样本ID强制为字符串,防止纯数字ID被识别为int
    "read_count": "int32",   # 用32位整数代替默认64位,省一半内存
})

# === 写出到文件 ===
df_tsv.to_csv("output.tsv", sep="\t", index=False)  # index=False 不写行号
df_tsv.to_excel("output.xlsx", index=False)          # 写 Excel

2. 筛选过滤

import pandas as pd

# 构造示例:样本元数据表
metadata = pd.DataFrame({
    "sample_id": ["S01", "S02", "S03", "S04", "S05"],
    "group": ["T2D", "Healthy", "T2D", "Healthy", "T2D"],
    "age": [55, 30, 62, 28, 45],
    "bmi": [28.5, 22.1, 31.0, 21.5, 27.8],
})

# === 单条件筛选:只要糖尿病组 ===
t2d_samples = metadata[metadata["group"] == "T2D"]
# 白话:方括号里放布尔条件,True 的行留下

# === 多条件筛选:糖尿病组 且 年龄>50 ===
# 注意:多条件必须用 & (且) 或 | (或),不能用 and/or
# 每个条件要加小括号
t2d_old = metadata[(metadata["group"] == "T2D") & (metadata["age"] > 50)]

# === isin:筛选多个值 ===
# 比如只要 S01 和 S03 两个样本
subset = metadata[metadata["sample_id"].isin(["S01", "S03"])]

# === query 方法(更像 SQL 的写法)===
result = metadata.query("group == 'T2D' and bmi > 28")

# === 按列值排序 ===
sorted_df = metadata.sort_values("age", ascending=False)  # 年龄从大到小

3. 分组聚合 groupby

import pandas as pd

# 构造示例:物种丰度表(长格式)
abundance = pd.DataFrame({
    "sample_id": ["S01", "S01", "S02", "S02", "S03", "S03"],
    "species": ["E.coli", "B.fragilis", "E.coli", "B.fragilis", "E.coli", "B.fragilis"],
    "group": ["T2D", "T2D", "Healthy", "Healthy", "T2D", "T2D"],
    "relative_abundance": [0.15, 0.08, 0.05, 0.12, 0.20, 0.06],
})

# === 按疾病分组,算每组的平均丰度 ===
group_mean = abundance.groupby("group")["relative_abundance"].mean()
# 结果:Healthy 0.085, T2D 0.1225
# 白话:先按 group 列分堆,再对 relative_abundance 列求均值

# === 按疾病+物种交叉分组 ===
cross_mean = abundance.groupby(["group", "species"])["relative_abundance"].mean()

# === 同时算多个统计量 ===
stats = abundance.groupby("group")["relative_abundance"].agg(["mean", "std", "count"])
# 结果是一个表,列为 mean / std / count

# === 自定义聚合函数 ===
# 比如算每组丰度的变异系数 CV = std / mean
cv = abundance.groupby("group")["relative_abundance"].agg(
    lambda x: x.std() / x.mean()  # lambda 匿名函数
)

4. 合并 merge / concat

import pandas as pd

# === 示例数据 ===
# 表1:样本元数据
metadata = pd.DataFrame({
    "sample_id": ["S01", "S02", "S03"],
    "group": ["T2D", "Healthy", "T2D"],
})

# 表2:物种丰度(宽格式,每列一个样本)
abundance = pd.DataFrame({
    "species": ["E.coli", "B.fragilis", "F.prausnitzii"],
    "S01": [1500, 800, 200],
    "S02": [500, 1200, 3000],
    "S03": [2000, 600, 150],
})

# === merge:按共同列合并(类似 SQL 的 JOIN)===
# 把元数据和丰度表通过 sample_id 连起来
# 先把丰度表从宽格式转为长格式
abundance_long = abundance.melt(
    id_vars="species",           # 保持不动的列
    var_name="sample_id",        # 原来的列名变成这一列的值
    value_name="read_count",     # 原来的数值放这一列
)
# 现在可以 merge 了
merged = abundance_long.merge(metadata, on="sample_id", how="left")
# how="left":以左表为基准,左表全保留,右表匹配不上的填 NaN
# how="inner":只保留两边都有的(默认)
# how="outer":全保留,匹配不上的填 NaN

# === concat:纵向拼接(上下叠加)===
# 合并多个样本的 Kraken2 输出
df1 = pd.DataFrame({"species": ["E.coli"], "count": [100], "sample": ["S01"]})
df2 = pd.DataFrame({"species": ["E.coli"], "count": [200], "sample": ["S02"]})
combined = pd.concat([df1, df2], ignore_index=True)
# ignore_index=True:重置行号,不然会有重复的 0, 0

# === concat:横向拼接(左右并排)===
left = pd.DataFrame({"A": [1, 2]})
right = pd.DataFrame({"B": [3, 4]})
side_by_side = pd.concat([left, right], axis=1)  # axis=1 表示按列拼

5. 数据透视 pivot

import pandas as pd

# 长格式丰度表 → 宽格式丰度矩阵(生信最常用的转换)
abundance_long = pd.DataFrame({
    "sample_id": ["S01", "S01", "S02", "S02"],
    "species": ["E.coli", "B.fragilis", "E.coli", "B.fragilis"],
    "read_count": [1500, 800, 500, 1200],
})

# === pivot:长转宽 ===
wide = abundance_long.pivot(
    index="species",        # 行:物种
    columns="sample_id",    # 列:样本
    values="read_count",    # 填的值:reads数
)
# 结果就是标准的物种-样本丰度矩阵

# === pivot_table:带聚合的透视(有重复值时用)===
# 如果同一个 sample+species 有多条记录,可以指定聚合方式
wide_agg = abundance_long.pivot_table(
    index="species",
    columns="sample_id",
    values="read_count",
    aggfunc="sum",          # 重复的加起来;也可以用 "mean"
    fill_value=0,           # 缺失位置填 0
)

# === melt:宽转长(pivot 的逆操作)===
long_again = wide.reset_index().melt(
    id_vars="species",
    var_name="sample_id",
    value_name="read_count",
)

6. apply 自定义函数

import pandas as pd
import numpy as np

abundance = pd.DataFrame({
    "species": ["E.coli", "B.fragilis", "Unknown_sp1"],
    "S01": [1500, 800, 50],
    "S02": [500, 1200, 30],
})

# === 对单列 apply ===
# 给物种名加前缀
abundance["species_fmt"] = abundance["species"].apply(
    lambda x: f"s__{x}"  # 类似 MetaPhlAn 的格式
)

# === 对每一行 apply(axis=1)===
# 判断某物种在多少个样本中检出(count > 0)
abundance["prevalence"] = abundance[["S01", "S02"]].apply(
    lambda row: (row > 0).sum(),  # 每行统计大于0的个数
    axis=1,                        # axis=1 表示逐行操作
)

# === 对整列做向量化操作(推荐,比 apply 快 10-100 倍)===
# 计算 log10 变换(加伪计数避免 log(0))
sample_cols = ["S01", "S02"]
abundance[sample_cols] = np.log10(abundance[sample_cols] + 1)
# 白话:能用 numpy 向量化就不要用 apply,速度差距巨大

7. 缺失值处理

import pandas as pd
import numpy as np

# 生信中缺失值常见场景:某物种在某样本中未检出
df = pd.DataFrame({
    "species": ["E.coli", "B.fragilis", "Archaea_sp"],
    "S01": [1500, np.nan, 50],     # B.fragilis 在 S01 未检出
    "S02": [500, 1200, np.nan],    # Archaea_sp 在 S02 未检出
})

# === 检查缺失值 ===
print(df.isnull().sum())        # 每列有几个缺失
print(df.isnull().sum().sum())  # 总共几个缺失

# === 填充缺失值 ===
df_filled = df.fillna(0)       # 未检出的填 0(生信最常用)

# === 按列的中位数填充(适合元数据中的连续变量如 BMI)===
df["S01"] = df["S01"].fillna(df["S01"].median())

# === 删除含缺失值的行 ===
df_clean = df.dropna()                    # 任一列有 NaN 就删
df_clean2 = df.dropna(subset=["S01"])     # 只看 S01 列有没有 NaN

# === 替换特定值为 NaN(比如把 "NA"、"unclassified" 标记为缺失)===
df.replace({"unclassified": np.nan, "NA": np.nan}, inplace=True)

8. 字符串操作 str

import pandas as pd

# 生信中常见字符串处理:解析分类学层级
taxonomy = pd.DataFrame({
    "lineage": [
        "k__Bacteria|p__Firmicutes|g__Lactobacillus|s__L.rhamnosus",
        "k__Bacteria|p__Bacteroidetes|g__Bacteroides|s__B.fragilis",
        "k__Bacteria|p__Proteobacteria|g__Escherichia|s__E.coli",
    ]
})

# === 提取属名(genus)===
# 用正则从 g__ 到下一个 | 之间提取
taxonomy["genus"] = taxonomy["lineage"].str.extract(r"g__([^|]+)")
# [^|]+ 意思是:匹配一个或多个非 | 字符

# === 提取种名(species)===
taxonomy["species"] = taxonomy["lineage"].str.extract(r"s__(.+)$")

# === 按分隔符切割 ===
# 把 lineage 按 | 分割成多列
split_cols = taxonomy["lineage"].str.split("|", expand=True)
split_cols.columns = ["kingdom", "phylum", "genus", "species"]

# === 常用字符串方法 ===
taxonomy["lineage_lower"] = taxonomy["lineage"].str.lower()       # 全部小写
taxonomy["has_firm"] = taxonomy["lineage"].str.contains("Firmicutes")  # 是否包含
taxonomy["lineage_clean"] = taxonomy["lineage"].str.replace("k__", "")  # 替换

9. 时间序列

import pandas as pd

# 纵向队列研究:同一病人多时间点采样
timeline = pd.DataFrame({
    "sample_id": ["P01_D0", "P01_D7", "P01_D30", "P02_D0", "P02_D7"],
    "patient": ["P01", "P01", "P01", "P02", "P02"],
    "collection_date": ["2024-01-01", "2024-01-08", "2024-01-31",
                        "2024-02-15", "2024-02-22"],
    "shannon_diversity": [3.2, 2.8, 3.5, 4.1, 3.9],
})

# === 转为日期类型 ===
timeline["collection_date"] = pd.to_datetime(timeline["collection_date"])

# === 计算采样间隔天数 ===
timeline["days_since_first"] = timeline.groupby("patient")["collection_date"].transform(
    lambda x: (x - x.min()).dt.days  # 每个病人组内,算距第一次采样的天数
)

# === 按月统计采样数 ===
timeline["month"] = timeline["collection_date"].dt.month  # 提取月份
monthly_count = timeline.groupby("month")["sample_id"].count()

# === 设置日期为索引后可以切片 ===
timeline = timeline.set_index("collection_date")
jan_data = timeline.loc["2024-01"]  # 只取1月的数据

性能优化技巧

向量化 vs 循环

import pandas as pd
import numpy as np

# 构造 100万行 数据
n = 1_000_000
df = pd.DataFrame({
    "read_count": np.random.randint(0, 10000, n),
    "total_reads": np.random.randint(100000, 500000, n),
})

# ❌ 慢:用 for 循环逐行算相对丰度
# 不要这么写!处理 100 万行要好几秒
# for i in range(len(df)):
#     df.loc[i, "rel_ab"] = df.loc[i, "read_count"] / df.loc[i, "total_reads"]

# ❌ 中等:用 apply(比循环快一点,但还是逐行)
# df["rel_ab"] = df.apply(lambda row: row["read_count"] / row["total_reads"], axis=1)

# ✅ 快:向量化运算(整列直接除,底层是 C 实现)
df["rel_ab"] = df["read_count"] / df["total_reads"]
# 速度对比:向量化通常比 for 循环快 100 倍以上

dtypes 优化

import pandas as pd

df = pd.read_csv("big_abundance.tsv", sep="\t")

# 查看内存占用
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")

# === 优化策略 ===
# 1. 整数列降精度:int64 → int32 或 int16
df["read_count"] = df["read_count"].astype("int32")    # 省一半内存

# 2. 分类列用 category 类型(重复值多的字符串列)
df["group"] = df["group"].astype("category")            # "T2D"/"Healthy" 只存两次
df["phylum"] = df["phylum"].astype("category")           # 门级别最多几十种

# 3. 浮点数降精度:float64 → float32
df["relative_abundance"] = df["relative_abundance"].astype("float32")

# 优化后再看内存,通常能省 50-70%
print(df.memory_usage(deep=True).sum() / 1024**2, "MB")

大文件分块读取

import pandas as pd

# 文件太大一次读不进内存时,分块处理
# chunksize=10000 表示每次只读 1万行
chunks = []
for chunk in pd.read_csv("huge_abundance.tsv", sep="\t", chunksize=10000):
    # 在每个块内做过滤,只保留需要的数据
    filtered = chunk[chunk["relative_abundance"] > 0.01]
    chunks.append(filtered)

# 最后把过滤后的小块拼起来
result = pd.concat(chunks, ignore_index=True)
# 这样即使原文件有几十GB,也能处理

Polars 简介:Pandas 的高性能替代

Polars 是用 Rust 写的 DataFrame 库,API 风格类似 Pandas 但速度快 5-20 倍,原生支持多线程和惰性求值。

# 安装:pip install polars
import polars as pl

# === 读取 TSV ===
df = pl.read_csv("species_abundance.tsv", separator="\t")

# === 筛选 + 选列 ===
result = df.filter(
    (pl.col("group") == "T2D") & (pl.col("relative_abundance") > 0.01)
).select(["sample_id", "species", "relative_abundance"])
# pl.col("列名") 是 Polars 的表达式写法,代替了 Pandas 的 df["列名"]

# === groupby 聚合 ===
stats = df.group_by("group").agg([
    pl.col("relative_abundance").mean().alias("mean_ab"),     # 均值
    pl.col("relative_abundance").std().alias("std_ab"),       # 标准差
    pl.col("sample_id").n_unique().alias("n_samples"),        # 不重复样本数
])

# === 惰性求值(Lazy API,处理大数据更快)===
# 先描述操作,不立即执行;最后 .collect() 时才计算
lazy_result = (
    pl.scan_csv("huge_file.tsv", separator="\t")  # scan 而非 read,不加载全部
    .filter(pl.col("kingdom") == "Bacteria")
    .group_by("phylum")
    .agg(pl.col("read_count").sum())
    .sort("read_count", descending=True)
    .collect()  # 这一步才真正执行,Polars 会自动优化执行计划
)

# === Pandas ↔ Polars 互转 ===
pandas_df = df.to_pandas()              # Polars → Pandas
polars_df = pl.from_pandas(pandas_df)   # Pandas → Polars

什么时候用 Polars:文件 >1GB、需要多线程加速、对速度有硬性要求时。日常小数据用 Pandas 更方便(生态更成熟、教程更多)。


生信实战案例

案例 1:处理 Kraken2 分类报告

import pandas as pd

# Kraken2 标准输出格式(6列,无表头)
# 列含义:百分比、直接覆盖reads、该层级reads、分类层级、TaxID、物种名
col_names = ["pct", "reads_clade", "reads_direct", "rank", "taxid", "name"]

kraken = pd.read_csv(
    "sample01.kreport",
    sep="\t",
    header=None,              # 没有表头
    names=col_names,          # 手动指定列名
)

# 去掉物种名前后的空格(Kraken2 输出有缩进)
kraken["name"] = kraken["name"].str.strip()

# 只提取种级别(rank == "S")
species = kraken[kraken["rank"] == "S"].copy()

# 按 reads 数从大到小排序
species = species.sort_values("reads_clade", ascending=False)

# 取 Top20 物种
top20 = species.head(20)
print(top20[["name", "reads_clade", "pct"]])

案例 2:合并多样本丰度表

import pandas as pd
import os

# 假设每个样本有一个 TSV 文件:S01_abundance.tsv, S02_abundance.tsv ...
# 每个文件两列:species, read_count

input_dir = "kraken_outputs/"
all_samples = []

for filename in os.listdir(input_dir):
    if filename.endswith("_abundance.tsv"):
        # 从文件名提取样本ID
        sample_id = filename.replace("_abundance.tsv", "")

        # 读取该样本的丰度表
        df = pd.read_csv(
            os.path.join(input_dir, filename),
            sep="\t",
            names=["species", "read_count"],  # 如果没表头就手动指定
        )
        df["sample_id"] = sample_id  # 加一列标记是哪个样本
        all_samples.append(df)

# 纵向合并所有样本
combined = pd.concat(all_samples, ignore_index=True)

# 转为宽格式丰度矩阵(物种 × 样本)
abundance_matrix = combined.pivot_table(
    index="species",
    columns="sample_id",
    values="read_count",
    fill_value=0,            # 未检出的填 0
)

# 保存
abundance_matrix.to_csv("merged_abundance_matrix.tsv", sep="\t")

案例 3:计算相对丰度并过滤低丰度物种

import pandas as pd

# 读取合并好的丰度矩阵(行=物种,列=样本)
matrix = pd.read_csv("merged_abundance_matrix.tsv", sep="\t", index_col=0)

# === 计算相对丰度 ===
# 每个样本(列)的 reads 总数
col_sums = matrix.sum(axis=0)   # axis=0 按列求和

# 每个值除以该列总数 → 相对丰度(0-1之间)
rel_abundance = matrix.div(col_sums, axis=1)  # axis=1 表示按列广播
# 白话:matrix 的每一列,都除以那一列的总和

# 验证:每列之和应该等于 1
print(rel_abundance.sum(axis=0))

# === 过滤低丰度物种 ===
# 策略:在所有样本中平均相对丰度 < 0.01% 的物种删掉
mean_abundance = rel_abundance.mean(axis=1)  # 每个物种跨样本的平均
keep_species = mean_abundance[mean_abundance >= 0.0001].index  # 保留的物种名
rel_filtered = rel_abundance.loc[keep_species]

print(f"过滤前: {matrix.shape[0]} 个物种")
print(f"过滤后: {rel_filtered.shape[0]} 个物种")

# 保存
rel_filtered.to_csv("relative_abundance_filtered.tsv", sep="\t")

Pandas vs Polars vs R dplyr 对比

维度 Pandas Polars R dplyr
语言 Python Python (Rust底层) R
速度 中等 快 5-20x 中等 (data.table 很快)
内存 高(默认 64 位) 低(自动优化) 中等
多线程 不支持 原生支持 不支持 (data.table 支持)
惰性求值 有 (scan/lazy) 无 (dbplyr 有)
生态 最丰富 快速增长中 生信最成熟 (Bioconductor)
学习曲线 中(表达式语法不同) 低 (tidyverse 很直觉)
生信专用包 少(要自己写) 更少 最多 (phyloseq, DESeq2)
面试考频 高(Python岗必考) 低(但加分项) 高(R岗必考)

实际建议: - 面试准备:Pandas 必须熟练,面试最常考 - 日常分析:小数据 (<1GB) 用 Pandas,大数据用 Polars - R 包无替代时(如 DESeq2 差异分析),仍需切回 R


常见报错与解决

1. SettingWithCopyWarning

SettingWithCopyWarning: A value is trying to be set on a copy of a slice

原因:对切片后的 DataFrame 直接赋值,Pandas 不确定你是在改原表还是副本。

# ❌ 触发警告
df_sub = df[df["group"] == "T2D"]
df_sub["new_col"] = 1  # 警告!

# ✅ 正确写法:加 .copy()
df_sub = df[df["group"] == "T2D"].copy()
df_sub["new_col"] = 1  # 没问题

2. KeyError: 'column_name'

原因:列名写错了,或者列名有隐藏的空格/特殊字符。

# 排查:打印所有列名,检查有没有空格
print(df.columns.tolist())
# 如果发现列名有空格:
df.columns = df.columns.str.strip()  # 去掉所有列名的前后空格

3. MergeError 或合并后行数暴增

原因:merge 的 key 列有重复值,导致多对多笛卡尔积。

# 排查:检查 key 列是否有重复
print(df1["sample_id"].duplicated().sum())  # 看有几个重复
print(df2["sample_id"].duplicated().sum())

# 解决:先去重,或用 validate 参数让 Pandas 自动检查
merged = df1.merge(df2, on="sample_id", validate="one_to_one")
# validate 选项:one_to_one / one_to_many / many_to_one

4. TypeError: unsupported operand type 数值列实际是字符串

原因:CSV 中数字列被读成了字符串(比如有个值是 "NA" 或空格)。

# 排查
print(df["read_count"].dtype)  # 如果显示 object,说明是字符串

# 解决:强制转换,无法转换的变成 NaN
df["read_count"] = pd.to_numeric(df["read_count"], errors="coerce")
# errors="coerce":遇到不能转的值(如 "NA")就变成 NaN 而不报错

5. MemoryError 内存不够

原因:文件太大,一次性加载超出可用内存。

# 解决方案1:分块读取(见上面"大文件分块读取"章节)

# 解决方案2:只读需要的列
df = pd.read_csv("huge.tsv", sep="\t", usecols=["species", "read_count"])

# 解决方案3:换 Polars(内存更高效)
import polars as pl
df = pl.scan_csv("huge.tsv", separator="\t").collect()

6. ValueError: cannot reindex from a duplicate axis

原因:索引有重复值时做对齐操作。

# 排查
print(df.index.duplicated().sum())

# 解决:重置索引
df = df.reset_index(drop=True)  # drop=True 表示不把旧索引变成列

速查表

读写操作

操作 代码
读 CSV pd.read_csv("f.csv")
读 TSV pd.read_csv("f.tsv", sep="\t")
读 Excel pd.read_excel("f.xlsx")
预览前 N 行 pd.read_csv("f.csv", nrows=100)
只读指定列 pd.read_csv("f.csv", usecols=["a","b"])
写 TSV df.to_csv("f.tsv", sep="\t", index=False)

查看与描述

操作 代码
前 5 行 df.head()
形状 df.shape
列类型 df.dtypes
统计摘要 df.describe()
缺失值计数 df.isnull().sum()
内存占用 df.memory_usage(deep=True)
唯一值 df["col"].nunique()

筛选与排序

操作 代码
条件筛选 df[df["col"] > 5]
多条件 df[(df["a"]>1) & (df["b"]=="x")]
isin df[df["col"].isin(["a","b"])]
query df.query("col > 5")
排序 df.sort_values("col", ascending=False)
去重 df.drop_duplicates(subset=["col"])

分组与聚合

操作 代码
分组均值 df.groupby("g")["v"].mean()
多统计量 df.groupby("g")["v"].agg(["mean","std"])
分组计数 df.groupby("g").size()
分组排名 df.groupby("g")["v"].rank()

合并与重塑

操作 代码
左连接 df1.merge(df2, on="key", how="left")
纵向拼接 pd.concat([df1, df2], ignore_index=True)
长转宽 df.pivot(index=, columns=, values=)
宽转长 df.melt(id_vars=, var_name=, value_name=)

缺失值与类型

操作 代码
填充 0 df.fillna(0)
删缺失行 df.dropna()
强制转数值 pd.to_numeric(df["c"], errors="coerce")
转日期 pd.to_datetime(df["c"])
转 category df["c"].astype("category")

延伸资源

资源 说明
Pandas 官方文档 10 minutes to pandas 官方入门,看完就能用
Polars 官方 User Guide Polars 从零学习
Python for Data Analysis (Wes McKinney) Pandas 作者写的书,权威教材
Pandas Cheat Sheet (DataCamp) 一页纸速查,适合打印贴桌上
Bioinformatics with Python Cookbook 生信场景的 Pandas 实战
real-world Polars benchmarks (pola-rs/tpch) Polars 官方性能对比数据

面试自测题

  1. mergehow 参数有哪几种?分别什么含义?
  2. groupby 后用 transformagg 有什么区别?
  3. 为什么向量化运算比 apply 快?
  4. 如何处理一个 20GB 的 TSV 文件?
  5. SettingWithCopyWarning 是什么原因?怎么解决?
  6. 把物种丰度表从宽格式转为长格式用哪个函数?

提示:以上 6 题答案全在本文档中,面试前过一遍确保能脱口而出。