Skip to content

Pandas 速查表

用于 Python 的数据分析与处理库。

01

入门

安装与导入

pandas 按惯例导入为 pd,numpy 为 np,因为 pandas 底层使用 numpy 数组。两个核心数据结构是 Series(一维带标签数组)和 DataFrame(二维带标签表)。Python 中几乎所有表格数据操作都从这里开始。

pandas
# Install pandas (pick one)
pip install pandas
conda install pandas

# Standard import convention
import pandas as pd
import numpy as np  # pandas relies heavily on numpy

# Check version
print(pd.__version__)

# Pandas is built on numpy: a Series is a 1D labeled array,
# a DataFrame is a 2D labeled table (dict of Series).

Series 与 DataFrame

Series 是单列值加索引(标签)。DataFrame 是共享公共索引的 Series 集合——可把它想象成电子表格或 SQL 表。可从字典(键=列)、列表的列表、numpy 数组或文件构建 DataFrame。

pandas
import pandas as pd

# Series: 1D labeled array
s = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])
# a    10
# b    20
# ...

# DataFrame: 2D table (dict of columns)
df = pd.DataFrame({
    "name":  ["Alice", "Bob", "Charlie"],
    "age":   [25, 30, 35],
    "city":  ["NYC", "LA", "SF"],
})

# From list of lists with column names
df2 = pd.DataFrame([[1, 2], [3, 4]], columns=["a", "b"])

创建示例 DataFrame

快速实验时从字典构建小 DataFrame。较大测试用带种子的 numpy 随机生成器保证可复现。date_range 创建 DatetimeIndex——时间序列工作的基础。尽早设置有意义的索引(日期、ID)使后续选择和连接更干净。

pandas
import pandas as pd
import numpy as np

# Quick test DataFrame with random data
df = pd.DataFrame({
    "id":     range(1, 6),
    "name":   ["Alice", "Bob", "Charlie", "David", "Eve"],
    "age":    [25, 30, 35, 28, 40],
    "score":  [85.5, 92.0, 78.3, 88.9, 95.2],
    "pass":   [True, True, False, True, True],
})

# Larger random DataFrame
rng = np.random.default_rng(42)
big = pd.DataFrame(rng.normal(size=(100, 4)), columns=list("ABCD"))

# Dates as index
dates = pd.date_range("2024-01-01", periods=5)
ts = pd.DataFrame({"value": [1, 2, 3, 4, 5]}, index=dates)

从文件读取数据

read_csv 是主力——相关时务必指定 sep、encoding、parse_dates 和 index_col 以避免加载后清洗。Parquet 对大数据远比 CSV 快且小,并保留 dtype。SQL 用 SQLAlchemy 引擎处理连接池。read_clipboard 对快速复制粘贴分析出奇地好用。

pandas
import pandas as pd

# CSV — the most common
df = pd.read_csv("data.csv")
df = pd.read_csv("data.csv", sep=";", encoding="utf-8",
                 parse_dates=["date"], index_col="id")

# Excel (needs openpyxl)
df = pd.read_excel("data.xlsx", sheet_name="Sheet1")

# JSON
df = pd.read_json("data.json")

# Parquet (fast columnar format, great for large data)
df = pd.read_parquet("data.parquet")

# SQL (needs SQLAlchemy)
from sqlalchemy import create_engine
engine = create_engine("sqlite:///my.db")
df = pd.read_sql("SELECT * FROM users", engine)

# Clipboard (paste a table from a spreadsheet)
df = pd.read_clipboard()

保存数据

保存到 CSV/Excel 时始终传 index=False,除非索引携带有意义数据——否则重载时会得到多余的 'Unnamed: 0' 列。Parquet 跨保存/加载保留 dtype(CSV 不会)。用 if_exists='append' 向已有 SQL 表加行,'replace' 覆盖。

pandas
import pandas as pd

# CSV (no index by default is usually what you want)
df.to_csv("output.csv", index=False)
df.to_csv("output.csv", index=False, encoding="utf-8-sig")  # Excel-friendly

# Excel (needs openpyxl)
df.to_excel("output.xlsx", sheet_name="Sheet1", index=False)

# Parquet (preserves dtypes, fast, compressed)
df.to_parquet("output.parquet")

# JSON
df.to_json("output.json", orient="records", indent=2)

# SQL
df.to_sql("users", engine, if_exists="replace", index=False)

# Multiple sheets to one Excel file
with pd.ExcelWriter("multi.xlsx") as writer:
    df1.to_excel(writer, sheet_name="sheet1", index=False)
    df2.to_excel(writer, sheet_name="sheet2", index=False)
02

数据检视

查看数据:head、tail、sample

head/tail 显示边缘;sample 显示随机行,对分布和格式做合理性检查更具代表性。用 set_option 禁用宽 DataFrame 检视时的截断——默认会隐藏列和行。任何分析前都看一眼样本数据,以尽早发现编码问题、哨兵值和类型不匹配。

pandas
import pandas as pd

# First/last rows
df.head()        # first 5 (default)
df.head(10)      # first 10
df.tail()        # last 5
df.tail(3)       # last 3

# Random sample (great for sanity checks)
df.sample(5)                    # 5 random rows
df.sample(frac=0.1, random_state=42)  # 10% of rows
df.sample(n=5, replace=True)    # sample with replacement (bootstrap)

# See the raw values (not truncated)
pd.set_option("display.max_rows", 200)
pd.set_option("display.max_columns", None)
pd.set_option("display.width", 1000)

结构与 Dtype

info() 是最有用的检视调用——它显示 dtype、每列非空计数和内存。留意 object dtype(通常字符串存储低效);转成 'category' 或 'string' dtype 省内存。memory_usage(deep=True) 揭示 object 列的真实开销,常比报告的大得多。

pandas
# Shape and basic structure
df.shape         # (rows, cols)
df.size          # rows * cols
df.ndim          # 2 for DataFrame, 1 for Series
len(df)          # number of rows

# Column names and dtypes
df.columns       # column labels
df.index         # row labels
df.dtypes        # dtype of each column
df.info()        # concise summary: dtypes, non-null counts, memory

# Memory usage (helpful for big data)
df.memory_usage(deep=True)
df.info(memory_usage="deep")

描述性统计

describe() 给出快速统计摘要——留意不匹配的计数(缺失值)和可疑的 min/max。value_counts() 对类别列必不可少(显示频率和类别平衡)。corr() 默认 Pearson;传 method='spearman' 做秩相关。混合类型帧始终用 numeric_only=True 避免错误。

pandas
# Numeric summary (count, mean, std, min, quartiles, max)
df.describe()
df.describe(include="all")   # include non-numeric too
df.describe(percentiles=[.1, .5, .9])

# Per-column stats
df["age"].mean()
df["age"].median()
df["age"].std()
df["age"].var()
df["age"].min(), df["age"].max()
df["age"].quantile(0.95)

# Non-numeric summaries
df["city"].value_counts()
df["city"].unique()
df["city"].nunique()

# Correlation and covariance
df.corr(numeric_only=True)
df.cov(numeric_only=True)

唯一值与计数

value_counts 是最常用的 pandas 方法之一——搭配 normalize=True 得比例,dropna=False 检查缺失。crosstab 构建两个类别列的频率表,对卡方检验和混淆矩阵有用。连续列分箱后再 value_counts 揭示分布形状。

pandas
# Unique values
df["city"].unique()              # array of unique values
df["city"].nunique()             # count of unique
df["city"].nunique(dropna=False) # include NaN

# Value counts (frequency table)
df["city"].value_counts()
df["city"].value_counts(normalize=True)   # proportions
df["city"].value_counts(ascending=True)
df["city"].value_counts(dropna=False)     # count NaN too

# Cross tabulation (frequency table of two columns)
pd.crosstab(df["city"], df["pass"])

# Binning then counting
df["age_group"] = pd.cut(df["age"], bins=[0, 18, 30, 50, 100],
                          labels=["child", "young", "adult", "senior"])
df["age_group"].value_counts()

检查缺失值

分析前始终检查缺失——每列 isnull().sum() 是标准的第一眼。关注缺失比例高的列;可能需丢弃或不同处理策略。关键列有缺失的行可用 df[df.isnull().any(axis=1)] 筛选。missingno 库可视化缺失模式以发现结构性数据问题。

pandas
# Count missing per column
df.isnull().sum()
df.isna().sum()           # alias

# Fraction missing
df.isnull().mean().mul(100).round(2)  # % missing per column

# Total missing
df.isnull().sum().sum()

# Rows with any missing
df[df.isnull().any(axis=1)]

# Non-null counts
df.notnull().sum()

# Visualize missingness pattern
# (requires missingno: import missingno as msno; msno.matrix(df))
03

Series 基础

创建 Series

Series 是带标签(索引)的一维数组。索引让你按标签查值,不只是按位置。指定更小 dtype(int8、float32)在大数据上省内存——int8 持 -128..127,对布尔转整数或小编码足够。字典键自然成为索引。

pandas
import pandas as pd
import numpy as np

# From a list
s = pd.Series([1, 2, 3, 4])

# From a list with custom index
s = pd.Series([10, 20, 30], index=["a", "b", "c"])

# From a dict (keys become index)
s = pd.Series({"a": 1, "b": 2, "c": 3})

# From a numpy array
s = pd.Series(np.random.randn(5))

# Scalar broadcast to an index
s = pd.Series(5, index=["a", "b", "c"])  # [5, 5, 5]

# Specify dtype
s = pd.Series([1, 2, 3], dtype="float64")
s = pd.Series([1, 2, 3], dtype="int8")    # save memory

索引与选择

基于标签的切片(s['a':'c'])两端都包含——这是与 Python 位置切片(排除右端)相比的常见坑。用 .loc 选标签、.iloc 选位置以避免歧义,尤其整数索引时。默认 s[...] 有重载(标签或位置取决于 dtype),所以生产代码用显式 .loc/.iloc 更安全。

pandas
s = pd.Series([10, 20, 30, 40], index=["a", "b", "c", "d"])

# By label (index)
s["a"]            # 10
s[["a", "c"]]     # multiple labels -> Series
s["a":"c"]        # slice INCLUSIVE of both ends (label-based)

# By position
s.iloc[0]         # first by position
s.iloc[-1]        # last
s.iloc[1:3]       # EXCLUSIVE of end (positional, like Python)

# By label with .loc (explicit)
s.loc["a"]
s.loc["a":"c"]    # inclusive

# Boolean indexing
s[s > 20]
s[(s > 10) & (s < 40)]

向量化运算

Series 运算向量化(无 Python 循环)且按索引对齐——两 Series 相加时,pandas 匹配标签,标签不重叠处填 NaN。此索引对齐是 pandas 的杀手锏但可能让你意外;用 .values 在原始 numpy 数组上做纯位置数学。向量化运算比 iterrows 循环快 100-1000 倍。

pandas
s = pd.Series([1, 2, 3, 4])

# Element-wise arithmetic (aligns on index)
s + 10          # [11, 12, 13, 14]
s * 2
s ** 2
1 / s           # [1.0, 0.5, 0.333, 0.25]

# Between two Series (aligns on index, NaN for mismatches)
s1 = pd.Series({"a": 1, "b": 2, "c": 3})
s2 = pd.Series({"b": 20, "c": 30, "d": 40})
s1 + s2         # a=NaN, b=22, c=33, d=NaN

# Comparison and boolean
s > 2           # [False, False, True, True]
s.between(2, 3) # inclusive

# Math methods
s.sum(), s.mean(), s.std(), s.cumsum()

常用 Series 方法

sort_values 和 rank 是日常工具。drop_duplicates 默认保留首次出现;用 keep='last' 保留最后。clip 在绘图前封顶异常值很方便。多数方法默认跳过 NaN;传 skipna=False 传播 NaN。许多方法返回新 Series(pandas 大多不可变),所以可链式:s.dropna().sort_values().head(10)。

pandas
s = pd.Series([3, 1, 4, 1, 5, 9, 2, 6, np.nan])

# Sorting
s.sort_values()                  # ascending
s.sort_values(ascending=False)
s.sort_index()

# Ranking
s.rank(method="average")         # 'min','max','first','dense'

# Unique, counts
s.unique()
s.nunique()
s.value_counts()
s.duplicated()                   # boolean: True if seen before
s.drop_duplicates()

# Statistical
s.sum(), s.prod(), s.cumsum(), s.cummax()
s.abs()
s.round(2)
s.clip(lower=0, upper=10)        # cap values to a range
s.quantile([0.25, 0.5, 0.75])

处理 Series 中的 NaN

fillna 有多种策略:常量、统计量(均值/中位数)、前向/后向填充(时间序列绝佳),或插值。ffill/bfill 传播最后已知值——对不该跳变的传感器或股票数据理想。读取时用 na_values 把哨兵('?''、-999)转 NaN 以统一处理。interpolate 支持线性、时间和索引方法。

pandas
s = pd.Series([1.0, 2.0, np.nan, 4.0, np.nan])

# Detect
s.isna()             # boolean mask
s.notna()
s.isna().sum()       # count of NaN

# Drop
s.dropna()           # remove NaN

# Fill
s.fillna(0)                       # constant
s.fillna(s.mean())                # with mean
s.fillna(method="ffill")          # forward fill (old API)
s.ffill()                         # forward fill (new)
s.bfill()                         # backward fill
s.interpolate()                   # linear interpolation

# Replace sentinel values with NaN on read
# pd.read_csv("x.csv", na_values=["?", "N/A", ""])
04

选择与筛选

选择列

括号表示 df['col'] 总是有效;点表示 df.col 方便但对含空格或与方法同名(如 'count')的名称失效。filter(like=) 和 select_dtypes 是无需逐个列名抓取列组的干净方式——对含数百列的宽 DataFrame 极有用。

pandas
# Single column (returns Series)
df["age"]
df.age            # only if name is a valid Python identifier

# Multiple columns (returns DataFrame)
df[["name", "age", "city"]]

# Columns by pattern
df.filter(like="age")               # columns containing "age"
df.filter(regex="^\d{4}$")          # columns matching regex
df.filter(items=["name", "age"])    # specific columns

# Select by dtype
df.select_dtypes(include="number")
df.select_dtypes(include=["int64", "float64"])
df.select_dtypes(exclude="object")
df.select_dtypes(include="datetime")

loc vs iloc

pandas 头号坑:.loc 用标签(包含切片),.iloc 用位置(排除切片)。赋值时务必用 .loc/.iloc——链式索引如 df[df.a<5]['b']=0 会抛 SettingWithCopyWarning 且可能静默失败。.loc 支持布尔掩码(基于标签筛选),这是筛选并赋值的干净方式。

pandas
# .loc: label-based (inclusive slices)
df.loc[5]                         # row with index label 5
df.loc[5, "name"]                 # row 5, column 'name'
df.loc[5:10, ["name", "age"]]     # rows 5-10 (inclusive), 2 cols
df.loc[df["age"] > 30]            # boolean mask

# .iloc: position-based (exclusive slices)
df.iloc[0]                        # first row
df.iloc[-1]                       # last row
df.iloc[0:5]                      # first 5 rows (exclusive)
df.iloc[0:5, 0:2]                 # first 5 rows, first 2 cols
df.iloc[:, 0]                     # all rows, first column

# Setting values (use .loc for assignment!)
df.loc[df["age"] < 0, "age"] = 0  # cap negatives to 0

# AVOID chained indexing: df[df["a"]<5]["b"] = 0  # SettingWithCopyWarning

布尔筛选

组合条件时用 & | ~ 并每个条件加括号——Python 的 and/or 对布尔数组无效。isin 是按多值筛选的干净方式(比链式 == 快)。字符串筛选用 .str 访问器。用 ~ 取反(按位非)。这些都返回新的筛选 DataFrame;赋值给变量以保留。

pandas
# Single condition
df[df["age"] > 30]
df[df["city"] == "NYC"]

# Multiple conditions (use & | ~, NOT and/or/not; parenthesize!)
df[(df["age"] > 25) & (df["city"] == "NYC")]
df[(df["city"] == "NYC") | (df["city"] == "LA")]
df[~df["pass"]]                       # NOT pass

# isin for membership
df[df["city"].isin(["NYC", "LA", "SF"])]

# String methods in filters
df[df["name"].str.startswith("A")]
df[df["name"].str.contains("li", case=False)]

# Between
df[df["age"].between(25, 35, inclusive="both")]

# Null checks
df[df["score"].isnull()]
df[df["score"].notnull()]

query() 方法

query() 读起来像 SQL 且避免了布尔索引的处处括号要求。用 @ 引用本地 Python 变量。大 DataFrame 上 query 可能更快,因为它用 numexpr 求值,避免创建中间布尔数组。在 groupby 链和管道内尤其好用。

pandas
# query() — string-based filtering, often more readable
df.query("age > 30")
df.query("age > 25 and city == 'NYC'")
df.query("age > 25 and city in ['NYC', 'LA']")
df.query("age > 25 or score > 90")

# Refer to Python variables with @
cities = ["NYC", "LA"]
df.query("city in @cities")

# Compare columns
df.query("score > age")

# Negation
df.query("not pass")

# query is faster than boolean indexing on large frames
# because it uses numexpr to avoid intermediate arrays

采样与加权采样

sample(frac=0.8) 后 drop(train.index) 是保留原始索引的干净训练/测试划分。分层采样(保类别比例)用 sklearn 的 train_test_split 比手动 pandas 代码可靠。加权采样对重要性采样或模拟有偏数据有用。

pandas
# Random sample
df.sample(n=5)                          # 5 rows
df.sample(frac=0.1)                     # 10% of rows
df.sample(n=5, random_state=42)         # reproducible

# Sample with weights
df.sample(n=5, weights="score")         # higher score -> more likely

# Sample with replacement (bootstrap)
df.sample(n=len(df), replace=True)

# Train/test split
train = df.sample(frac=0.8, random_state=42)
test = df.drop(train.index)

# Stratified sample (preserve class proportions)
from sklearn.model_selection import train_test_split
train, test = train_test_split(df, test_size=0.2,
                                stratify=df["city"], random_state=42)
05

数据清洗

处理缺失数据

dropna(how='all') 只丢全空行;thresh 保留至少 N 个非空值的行。用字典按列填充比多次 fillna 调用干净。时间序列用 ffill/bfill 和插值保持连续性。始终考虑缺失本身是否有信息量——填充前加 'is_missing' 指示列可为 ML 保留该信号。

pandas
# Drop rows with any missing
df.dropna()                       # any NaN in any column
df.dropna(how="all")              # only if ALL columns NaN
df.dropna(subset=["age", "city"]) # only check these columns
df.dropna(thresh=3)               # keep rows with >=3 non-NaN

# Fill missing
df.fillna(0)
df.fillna({"age": df["age"].mean(),
           "city": "Unknown"})
df["age"] = df["age"].fillna(df["age"].median())

# Forward/backward fill (for time series)
df.ffill()
df.bfill()

# Interpolate
df.interpolate(method="linear")
df.interpolate(method="time")     # for DatetimeIndex

# Flag rows that were imputed
df["age_was_missing"] = df["age"].isnull()

移除重复

duplicated() 标记重复前序行的行;keep='last' 改为标记首次出现。keep=False 标记每个重复行(含首个),想检查所有冲突行时有用。drop_duplicates 返回新 DataFrame;传 inplace=True 或重新赋值以修改。subset 让你按关键列(如 email 或 ID)去重。

pandas
# Find duplicates
df.duplicated()                   # True for rows that are duplicates of earlier
df.duplicated().sum()             # count
df[df.duplicated()]               # view duplicates

# Drop duplicates
df.drop_duplicates()              # full-row duplicates
df.drop_duplicates(subset=["email"])   # dup on email only
df.drop_duplicates(subset=["name", "dob"], keep="last")  # keep last occurrence
df.drop_duplicates(keep=False)    # drop ALL duplicates (keep none)

# Find rows where a column is duplicated
df[df.duplicated(subset=["email"], keep=False)]
# .keep=False marks ALL duplicates (including first)

重命名与重排

带 columns 字典的 rename 是最安全的重命名方式——只重命名列出的列,其余不动。重排列只是按所需顺序选择。set_index 后 reset_index(drop=True) 是替换乱索引的干净方式。尽早用 str.strip 处理列名——不可见的尾部空格导致无尽 'KeyError' 头疼。

pandas
# Rename columns
df.rename(columns={"old_name": "new_name"})
df.rename(columns=str.upper)        # uppercase all
df.rename(columns=str.strip)        # strip whitespace

# Replace all column names
df.columns = ["id", "name", "age"]

# Add prefix/suffix
df.add_prefix("col_")
df.add_suffix("_v2")

# Reorder columns
df[["name", "age", "city"]]          # select in new order

# Reorder rows by index
df.sort_index()

# Reset / set index
df.reset_index(drop=True)            # drop old index
df.set_index("id")                   # use 'id' as index

# Swap rows and columns
df.transpose()  # or df.T

类型转换

astype 对不可解析值会失败;pd.to_numeric 配 errors='coerce' 把坏值转 NaN——对脏数据安全得多。'category' dtype 对低基数字符串省大量内存。降序(int64->int8)对小值范围列可省 8 倍内存。日期务必用显式格式字符串解析以快速正确。

pandas
# Convert a column's dtype
df["age"] = df["age"].astype(int)
df["price"] = df["price"].astype(float)
df["pass"] = df["pass"].astype(bool)
df["city"] = df["city"].astype("category")    # save memory

# Convert with pd.to_numeric (handles bad values)
df["price"] = pd.to_numeric(df["price"], errors="coerce")  # bad -> NaN
df["price"] = pd.to_numeric(df["price"], errors="ignore")  # leave as-is

# Parse dates
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")

# Convert multiple columns at once
df = df.astype({"age": "int8", "score": "float32", "city": "category"})

# Downcast to save memory
df["age"] = pd.to_numeric(df["age"], downcast="integer")

替换值

replace 是把哨兵值(-999、'?')换成 NaN 再分析的主力。map 按元素应用字典查找(未匹配值变 NaN)。where 保留条件为 True 的值并替换其余;mask 是其反。这些都返回新对象——重新赋值以应用。

pandas
# Replace specific values
df["city"].replace({"NYC": "New York", "LA": "Los Angeles"})
df.replace(-999, np.nan)              # whole frame
df.replace(["?", "N/A", ""], np.nan)  # multiple sentinels

# Replace by condition (mask)
df.loc[df["age"] < 0, "age"] = np.nan

# Clip outliers
df["age"] = df["age"].clip(0, 120)

# Map values with a dict (like a lookup)
df["grade"] = df["score"].map({90: "A", 80: "B", 70: "C"})

# Where: keep where condition True, else replace
df["age"].where(df["age"] >= 0, 0)    # set negatives to 0

# Mask: opposite of where
df["age"].mask(df["age"] < 0, 0)
06

数据变换

apply 与 applymap

apply 按元素/行运行 Python 函数——灵活但慢(比向量化慢 100 倍)。可能时始终优先向量化运算(df['col'] + 1、.str.upper())。apply 用于无法向量化的逻辑(自定义字符串解析、引用多列的条件变换)。逐行用 axis=1 并通过 row['col'] 访问列。

pandas
# Apply a function to each element of a column (Series)
df["age"] = df["age"].apply(lambda x: x + 1)
df["name"] = df["name"].apply(str.upper)

# Apply with multiple args
df["score"] = df["score"].apply(lambda x, n: round(x, n), n=2)

# Apply a function to each ROW (axis=1) — use columns
df["full"] = df.apply(lambda row: f"{row['name']} ({row['age']})", axis=1)

# Apply to every element of a DataFrame (newer: .map)
df = df.map(lambda x: str(x).strip() if isinstance(x, str) else x)

# Vectorized alternative (MUCH faster than apply)
df["age"] = df["age"] + 1  # vectorized, prefer this
df["name"] = df["name"].str.upper()

添加与修改列

np.where 是向量化的 if-else——远快于带 lambda 的 apply。np.select 处理多条件如 case/when 语句。assign 是链式友好的加列方式(管道中好用)。insert 在特定位置插入列。用 drop(columns=[...]) 丢列——比 del df['col'] 清晰得多。

pandas
# Add a new column
df["full_name"] = df["first"] + " " + df["last"]
df["is_adult"] = df["age"] >= 18

# Conditional column with np.where
df["category"] = np.where(df["age"] >= 18, "adult", "minor")

# Multiple conditions with np.select
conditions = [df["age"] < 18, df["age"] < 65, df["age"] >= 65]
choices = ["minor", "adult", "senior"]
df["group"] = np.select(conditions, choices, default="unknown")

# insert at a specific position
df.insert(2, "score_pct", df["score"] / 100)  # at col index 2

# Assign multiple columns at once (returns new df)
df = df.assign(
    age_months=df["age"] * 12,
    score_squared=df["score"] ** 2,
)

# Drop columns
df.drop(columns=["temp", "junk"])

分箱与离散化

cut 把范围分成等宽箱(可能产生空箱);qcut 分成等频箱(每箱约同计数)——对偏斜数据通常更有用。right=False 使箱左闭右开 [0,18) 而非右闭左开 (0,18]。分箱列变 category dtype,适合 groupby 聚合。

pandas
# Equal-width bins
df["age_bin"] = pd.cut(df["age"], bins=4)

# Custom bins with labels
df["age_group"] = pd.cut(
    df["age"],
    bins=[0, 18, 30, 50, 100],
    labels=["child", "young", "adult", "senior"],
    right=False,  # left-inclusive: [0, 18)
)

# Equal-frequency bins (quantiles)
df["income_quartile"] = pd.qcut(df["income"], q=4, labels=["Q1","Q2","Q3","Q4"])

# Get bin edges
df["age_bin"].value_counts(sort=False)
pd.cut(df["age"], bins=4).cat.categories  # see the bin ranges

字符串操作(.str 访问器)

.str 访问器向量化 Python 字符串方法——绝不用 apply(str.upper) 循环。split 配 expand=True 把一列变多列(拆分姓名绝佳)。extract 配命名组从文本中提取结构化数据。所有 .str 方法对 NaN 输入返回 NaN 并接受 na= 控制该行为。

pandas
# All vectorized string methods via .str
df["name"] = df["name"].str.upper()
df["name"] = df["name"].str.lower()
df["name"] = df["name"].str.strip()           # remove leading/trailing
df["name"] = df["name"].str.replace(" ", "_")
df["name"] = df["name"].str.title()           # Title Case

# Split
df["name"].str.split(" ")                     # list per row
df[["first", "last"]] = df["name"].str.split(" ", expand=True)

# Contains / startswith / match (returns boolean)
df["name"].str.contains("li", case=False, na=False)
df["name"].str.startswith("A")
df["name"].str.match(r"^A\w+$")              # regex match

# Extract with regex groups
df["name"].str.extract(r"(?P<first>\w+)\s+(?P<last>\w+)")

# Length
df["name"].str.len()

日期与时间(.dt 访问器)

始终先用 pd.to_datetime 转换日期列,再用 .dt 访问组件。.dt 访问器解锁年/月/日/工作日/小时和时区操作。pandas 可直接把日期与字符串('2024-01-01')比较。时区感知的 datetime 用 tz_localize(加时区)和 tz_convert(换时区)——别混用朴素和感知 datetime。

pandas
df["date"] = pd.to_datetime(df["date"])

# Extract parts
df["year"]      = df["date"].dt.year
df["month"]     = df["date"].dt.month
df["day"]       = df["date"].dt.day
df["weekday"]   = df["date"].dt.day_name()    # 'Monday'
df["hour"]      = df["date"].dt.hour
df["is_weekend"] = df["date"].dt.dayofweek >= 5

# Date arithmetic
df["age_days"] = (pd.Timestamp("today") - df["date"]).dt.days

# Format back to string
df["date_str"] = df["date"].dt.strftime("%Y-%m-%d")

# Timezone handling
df["date_utc"] = df["date"].dt.tz_localize("UTC")
df["date_local"] = df["date_utc"].dt.tz_convert("US/Eastern")

# Filter by date
df[df["date"] > "2024-01-01"]
df[df["date"].between("2024-01-01", "2024-12-31")]
07

分组与聚合

基础 GroupBy

groupby 遵循拆分-应用-合并:按组键拆分行,每组应用函数,合并结果。groupby('city')['age'].mean() 返回按城市索引的 Series;带列表的 agg 返回 DataFrame。size() 计每组所有行(含 NaN 键);value_counts() 排除 NaN 并按频率排序。多组键创建层次索引。

pandas
# Split-Apply-Combine: group rows, compute per group
df.groupby("city")["age"].mean()         # one column, one agg
df.groupby("city")["age"].agg(["mean", "median", "std"])

# Multiple columns
df.groupby("city")[["age", "score"]].mean()

# Group by multiple columns (hierarchical)
df.groupby(["city", "pass"])["age"].mean()

# Quick counts per group
df.groupby("city").size()                # includes NaN groups
df["city"].value_counts()                # excludes NaN, sorted

# Iterate over groups
for name, group in df.groupby("city"):
    print(name, len(group))

多重聚合

命名聚合(pandas 0.25+)是最干净的方式——输出列得你指定的名而非 MultiIndex。语法是 new_col=('source_col', 'function')。lambda 自定义函数可用但比内置慢(mean、sum 等是 C 优化)。想要组键作普通列时务必 reset_index()。

pandas
# Different aggregations per column
df.groupby("city").agg({
    "age":   ["mean", "median", "std"],
    "score": ["min", "max", "count"],
})

# Named aggregations (cleaner output, no MultiIndex)
result = df.groupby("city").agg(
    avg_age=("age", "mean"),
    median_age=("age", "median"),
    n_people=("age", "count"),
    max_score=("score", "max"),
)

# Custom aggregation function
df.groupby("city").agg(
    age_range=("age", lambda x: x.max() - x.min()),
)

# Reset index to get a flat DataFrame
result = result.reset_index()

transform 与 filter

transform 很神奇——它返回与输入同长度的 Series,把每组结果广播到其成员。适合在原始行旁添加组级统计(每类别均值),或用组均值填缺失。filter 按每组布尔条件保留或丢弃整组(如丢样本太少的城市)。

pandas
# transform: broadcast group result back to original shape
df["age_mean_city"] = df.groupby("city")["age"].transform("mean")
df["age_dev"] = df["age"] - df.groupby("city")["age"].transform("mean")

# Fill missing with group mean
df["score"] = df.groupby("city")["score"].transform(
    lambda s: s.fillna(s.mean()))

# filter: keep/drop entire groups based on a condition
# keep groups with more than 2 rows
big_groups = df.groupby("city").filter(lambda g: len(g) > 2)
# keep groups where average score > 80
df.groupby("city").filter(lambda g: g["score"].mean() > 80)

分组 apply

groupby 上 apply 最灵活(每组跑任意函数)但最慢——可能时优先内置聚合(mean、sum)或 transform。函数返回 Series 时创建以 Series 索引为列的 DataFrame。idxmax 返回最大值的索引——用它查同行其他列。

pandas
# Apply any function per group (most flexible)
def top_scorer(g):
    return g.nlargest(1, "score")

df.groupby("city").apply(top_scorer)
df.groupby("city").apply(lambda g: g.nlargest(2, "score"))

# Normalize within group
df.groupby("city").apply(
    lambda g: (g["score"] - g["score"].mean()) / g["score"].std()
)

# Multiple results per group
def summarize(g):
    return pd.Series({
        "count": len(g),
        "avg_score": g["score"].mean(),
        "top_name": g.loc[g["score"].idxmax(), "name"],
    })

df.groupby("city").apply(summarize)

# include_groups=False (newer pandas) to exclude group cols from g

透视表与交叉表

pivot_table 是 groupby + unstack 的组合——把长数据转矩阵(如按城市 × 通过的平均分)完美。margins=True 加行列总计。crosstab 是专用于计数组合的透视(频率表);normalize='index' 给行比例。Melt 是其逆,宽转长格式。

pandas
# pivot_table: groupby + reshape in one step
pd.pivot_table(df, values="score", index="city", columns="pass",
               aggfunc="mean")
pd.pivot_table(df, values="score", index="city", columns="pass",
               aggfunc=["mean", "count"], margins=True)

# Multiple values
pd.pivot_table(df, values=["age", "score"], index="city",
               aggfunc={"age": "mean", "score": "max"})

# crosstab: frequency table of two (or more) categoricals
pd.crosstab(df["city"], df["pass"])
pd.crosstab(df["city"], df["pass"], normalize="index")  # row %
pd.crosstab(df["city"], df["pass"], values=df["score"],
            aggfunc="mean")  # like a pivot

# Melt a pivot back to long
wide = pd.pivot_table(df, values="score", index="city", columns="pass")
long = wide.reset_index().melt(id_vars="city", var_name="pass",
                                value_name="score")
08

合并与连接

merge(SQL 风格连接)

merge 是 pandas 的 SQL JOIN——how 控制 inner/left/right/outer/cross。始终显式指定 on;键名不同用 left_on/right_on。用 suffixes 消歧重叠列(默认 _x/_y 不透明)。validate='m:1'(或 '1:1'、'1:m'、'm:m')在连接基数错误时报错——防意外行重复的好护栏。

pandas
# Like SQL JOIN
pd.merge(df1, df2, on="id")                     # inner join (default)
pd.merge(df1, df2, on="id", how="left")         # left join
pd.merge(df1, df2, on="id", how="right")
pd.merge(df1, df2, on="id", how="outer")        # full outer
pd.merge(df1, df2, on="id", how="cross")        # cartesian product

# Different column names in each frame
pd.merge(df1, df2, left_on="user_id", right_on="id")

# Join on index
pd.merge(df1, df2, left_index=True, right_index=True)

# Suffixes for overlapping non-key columns
pd.merge(df1, df2, on="id", suffixes=("_left", "_right"))

# Validate to catch unexpected duplicates
pd.merge(df1, df2, on="id", validate="m:1")  # many-to-one expected

concat(堆叠)

concat 垂直(axis=0,默认)或水平(axis=1)堆叠 DataFrame。ignore_index=True 丢弃原索引创建干净 0..n-1——堆叠时通常是你想要的。join='inner' 只保留共享列(帧漂移时有用)。verify_integrity=True 对重复索引报错,是乱追加的安全网。

pandas
# Stack vertically (rows) — same columns
pd.concat([df1, df2])
pd.concat([df1, df2], ignore_index=True)   # reindex 0..n-1

# Stack horizontally (columns)
pd.concat([df1, df2], axis=1)

# Concat with keys (hierarchical index)
pd.concat([df1, df2], keys=["q1", "q2"])

# Append (deprecated; use concat)
# df1.append(df2)  # deprecated, use pd.concat

# Verify columns match when stacking
pd.concat([df1, df2], verify_integrity=True)  # error on dup index

# Join options for mismatched columns
pd.concat([df1, df2], join="inner")  # keep only shared columns
pd.concat([df1, df2], join="outer")  # keep all, NaN for missing

join(基于索引)

join 是 merge 对连接索引常见情况的语法糖——两帧都已有有意义索引时更简洁。按列连接时 merge 更清晰。join 可一次连接多个 DataFrame(传列表),把多个查找表合并到主帧很方便。

pandas
# join is merge optimized for joining on the index
df1.join(df2)                          # join on index
df1.join(df2, how="left")              # left, right, inner, outer
df1.join([df2, df3])                   # join multiple at once

# join on a column (set index first)
df1.set_index("id").join(df2.set_index("id"), how="inner")

# Suffix for overlapping columns
df1.join(df2, lsuffix="_left", rsuffix="_right")

# When to use join vs merge:
# - join: keys are the index (faster, cleaner)
# - merge: keys are regular columns (more flexible)

combine 与 update

combine_first 是从另一 DataFrame 填缺失值的惯用方式——像 SQL 的 coalesce。update 原地修改 df1,仅用 df2 的非 NaN 值覆盖。这些用较新部分数据修补主数据集很棒。concat + drop_duplicates 是快速 union/去重,但真正集合运算考虑带 indicator=True 的 merge。

pandas
# combine_first: fill NaN in df1 with values from df2
df1.combine_first(df2)
# Where df1 is NaN, take df2's value; otherwise keep df1.

# update: overwrite df1 in place with non-NaN from df2
df1.update(df2)

# combine with a custom function
import numpy as np
df1.combine(df2, lambda s1, s2: np.minimum(s1, s2))

# Concat then drop duplicates (union with dedup)
pd.concat([df1, df2]).drop_duplicates()

检查合并结果

indicator=True 对调试连接极有价值——它告诉你哪些行来自哪侧。合并后行数突增通常意味某帧有重复键;连接前用 value_counts() 检查键。简单『筛 df1 中键在 df2 中的行』用 isin(半连接)——无需实际 merge。

pandas
# indicator=True adds a '_merge' column showing the source
merged = pd.merge(df1, df2, on="id", how="outer", indicator=True)
# _merge values: 'left_only', 'right_only', 'both'

# Find rows in df1 but not df2 (anti-join)
only_left = merged[merged["_merge"] == "left_only"]

# Find rows in both (intersection)
both = merged[merged["_merge"] == "both"]

# Detect unexpected row explosion (duplicate keys)
print(len(df1), len(df2), len(merged))
# if len(merged) >> max(len(df1), len(df2)) you have duplicate keys

# isin for a simple semi-join (filter df1 to keys in df2)
df1[df1["id"].isin(df2["id"])]            # semi-join
df1[~df1["id"].isin(df2["id"])]           # anti-join
09

重塑数据

melt(宽转长)

melt 把宽格式数据(每实体一行,多测量列)转长格式(每测量一行),这是大多数 ML/统计库偏爱的整洁格式。id_vars 是保持原样的标识列;其余变 var/value 对。长格式更易 groupby、筛选和绘图。

pandas
# melt: unpivot columns into rows
df_wide = pd.DataFrame({
    "id": [1, 2],
    "q1": [10, 20],
    "q2": [15, 25],
    "q3": [20, 30],
})

long = df_wide.melt(id_vars="id", var_name="quarter", value_name="score")
#    id quarter  score
# 0   1      q1     10
# 1   2      q1     20
# ...

# Melt only specific columns
df_wide.melt(id_vars="id", value_vars=["q1", "q2"])

# Keep the wide columns too (with identifier)
df_wide.melt(id_vars="id", value_vars=["q1","q2","q3"],
             var_name="quarter", value_name="score",
             ignore_index=True)

pivot(长转宽)

pivot 无聚合地把长转宽——若有重复(索引,列)对会失败,此时用带 aggfunc 的 pivot_table。列名变 pivot 的列,可能令人困惑——reset_index() 和 columns.name=None 清理结果。pivot 是 melt 的逆。

pandas
# pivot: long to wide (without aggregation)
df_long = pd.DataFrame({
    "id": [1, 1, 1, 2, 2, 2],
    "quarter": ["q1", "q2", "q3", "q1", "q2", "q3"],
    "score": [10, 15, 20, 30, 35, 40],
})

wide = df_long.pivot(index="id", columns="quarter", values="score")
# quarter  q1  q2  q3
# id
# 1        10  15  20
# 2        30  35  40

# pivot fails on duplicate index/value combos — use pivot_table then
df_long.pivot_table(index="id", columns="quarter",
                    values="score", aggfunc="sum")

# Restore a regular column from the index
wide = wide.reset_index()
wide.columns.name = None

stack 与 unstack

stack/unstack 在 MultiIndex 对象的行和列之间移动层级——对重塑分组结果强大。多键 groupby 产生带 MultiIndex 的 Series;unstack 把一层转列得矩阵视图。stack 是其逆。这些是透视表底层的构建块。

pandas
# MultiIndex column example
df = pd.DataFrame({
    ("A", "x"): [1, 2],
    ("A", "y"): [3, 4],
    ("B", "x"): [5, 6],
})

# stack: turn column level into inner row index
stacked = df.stack()
# Now a Series with MultiIndex (row, col_level_2)

# unstack: inverse — turn row level into column level
stacked.unstack()

# Common use: reshape grouped result
g = df.groupby(["city", "year"])["score"].mean()
# city  year
# NYC   2023    80
# NYC   2024    85
# LA    2023    70

g.unstack("year")  # years become columns
#        2023  2024
# city
# NYC    80    85
# LA     70    NaN

展开列表

explode 把列表值列转每元素一行,复制其他列——对嵌套数据(JSON 数组、多标签记录)必不可少。这是分析前规范化列表值列的干净方式。反向(行合并成列表)用 groupby + apply(list)。

pandas
# explode: turn list-like values into separate rows
df = pd.DataFrame({
    "id": [1, 2],
    "tags": [["python", "ml"], ["sql", "db", "etl"]],
})
#              id           tags
# 0             1  [python, ml]
# 1             2  [sql, db, etl]

exploded = df.explode("tags")
#    id    tags
# 0   1  python
# 0   1      ml
# 1   2     sql
# 1   2      db
# 1   2    etl

# Useful after str.split
df["tags"] = df["tags_str"].str.split(",")
exploded = df.explode("tags")

# Aggregate back: groupby + list
exploded.groupby("id")["tags"].apply(list)

MultiIndex 操作

MultiIndex(层次索引)让你在 2D 表示高维数据。xs(横截面)在特定层级选择无需指定所有外层。swaplevel 和 sort_index(level=) 为不同视图重排层级。多数时候 reset_index() 把 MultiIndex 平回普通列,更易处理。

pandas
# Create a MultiIndex DataFrame
idx = pd.MultiIndex.from_tuples([("NYC", 2023), ("NYC", 2024),
                                  ("LA", 2023)], names=["city", "year"])
df = pd.DataFrame({"score": [80, 85, 70]}, index=idx)

# Select by outer level
df.loc["NYC"]              # all NYC rows
df.xs("NYC", level="city") # same, more explicit

# Select by inner level
df.xs(2023, level="year")

# Swap levels
df.swaplevel()

# Sort by level
df.sort_index(level="year")

# Reset one level back to a column
df.reset_index(level="year")

# Flatten columns after a groupby
g = df.groupby(["city", "year"]).mean().reset_index()
10

排序与排名

排序值

带列列表和匹配 ascending 列表的 sort_values 按字典序排序——主键先,平局用次键。na_position 控制 NaN 去哪(默认 'last')。排序稳定,等键保持原序。排序修改索引顺序;之后 reset_index(drop=True) 给干净连续索引。

pandas
# Sort by one column
df.sort_values("age")
df.sort_values("age", ascending=False)

# Sort by multiple columns (stable sort)
df.sort_values(["city", "age"], ascending=[True, False])
# city ascending, age descending within each city

# Put NaNs first or last
df.sort_values("score", na_position="first")

# In-place (modifies df)
df.sort_values("age", inplace=True)

# Sort by index
df.sort_index()
df.sort_index(axis=1)            # sort columns by name
df.sort_index(ascending=False)

# Reset index after sorting
df.sort_values("age").reset_index(drop=True)

排名

rank 对计算百分位、组内 Top-N 和非参数统计必不可少。method='dense' 给 1,2,2,3(无间隙)——奖牌或等级绝佳;'first' 按出现顺序破平。rank(pct=True) 返回百分位。groupby + rank 计算组内排名(如每区域销冠)。

pandas
# Default rank: average for ties (1, 2.5, 2.5, 4)
df["rank"] = df["score"].rank()

# Methods for handling ties
df["score"].rank(method="average")  # default
df["score"].rank(method="min")      # ties get the min rank
df["score"].rank(method="max")      # ties get the max rank
df["score"].rank(method="first")    # ties broken by order of appearance
df["score"].rank(method="dense")    # 1, 2, 2, 3 (no gaps)

# Ascending / descending
df["score"].rank(ascending=False)   # highest = rank 1

# Rank as percentile
df["pct"] = df["score"].rank(pct=True)

# Rank within groups
df["city_rank"] = df.groupby("city")["score"].rank(ascending=False)

nlargest 与 nsmallest

nlargest/nsmallest 已优化——内部用堆,所以只需几行时比排序整帧快(如从百万行取前 10)。『每组 Top N』用 groupby + apply(nlargest),但某些数据上比全排序 + groupby head 慢。keep 控制破平('first'、'last'、'all')。

pandas
# Top N rows — faster than sort_values + head on large data
df.nlargest(5, "score")            # top 5 by score
df.nlargest(5, "score", keep="first")  # tie-breaking
df.nsmallest(3, "age")             # youngest 3

# Top N per group
df.groupby("city").apply(lambda g: g.nlargest(2, "score"))

# Bottom N with multiple columns
df.nsmallest(5, ["age", "score"])

# Equivalent sort_values (slower for big data)
df.sort_values("score", ascending=False).head(5)

排序索引与列

sort_index(axis=1) 按字母排序列——合并后任意加列时好用。把关键列(如 'id'、'name')放前面用列表拼接:前列然后其余。显式列选择 df[[...]] 是重排和子集列的最清晰方式。

pandas
# Sort the row index
df.sort_index()
df.sort_index(ascending=False)

# Sort columns alphabetically
df.sort_index(axis=1)
df.sort_index(axis=1, ascending=False)

# Reorder columns explicitly
df[["name", "age", "city", "score"]]

# Reorder to a pattern
cols = sorted(df.columns)
df = df[cols]

# Move specific columns to the front
front = ["id", "name"]
df = df[front + [c for c in df.columns if c not in front]]

带键排序

sort_values(key=) 排序前对列应用函数——不区分大小写或按长度排序。有序 Categorical 列按定义的类别顺序排序,非字母——工作日、尺寸(S<M<L)或自定义业务顺序绝佳。自然排序(file1、file2、file10)需拆分数字的自定义键。

pandas
# Sort by a function of the values (key, like sorted())
df.sort_values("name", key=lambda s: s.str.lower())
# case-insensitive sort

# Sort by string length
df.sort_values("name", key=lambda s: s.str.len())

# Sort by a custom order (categorical)
df["city"] = pd.Categorical(df["city"],
    categories=["NYC", "LA", "SF"], ordered=True)
df.sort_values("city")  # follows the defined category order

# Natural sort (numbers in strings)
import re
def natural_key(s):
    return [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", s)]
df.sort_values("version", key=lambda s: s.map(natural_key))
11

日期时间操作

解析日期

知道格式时始终传显式格式字符串——比推断快 10 倍且避免歧义(01/02/2024 可能是 1 月 2 日或 2 月 1 日)。errors='coerce' 把不可解析值转 NaT(时间 NaN,Not a Time)。对含年/月/日列的 DataFrame 用 pd.to_datetime 从部分构造日期。

pandas
# Parse strings to datetime
df["date"] = pd.to_datetime(df["date"])
df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d")  # faster + safer

# Handle mixed formats
df["date"] = pd.to_datetime(df["date"], format="mixed")

# Coerce bad values to NaT (datetime NaN)
df["date"] = pd.to_datetime(df["date"], errors="coerce")

# Parse on read
df = pd.read_csv("data.csv", parse_dates=["date", "created_at"])

# Parse multiple columns into one
df["datetime"] = pd.to_datetime(df[["year", "month", "day"]])

# Timestamp (scalar)
pd.Timestamp("2024-06-15 14:30:00")
pd.Timestamp.now()
pd.Timestamp("2024-06-15").tz_localize("UTC")

date_range 与 Period

date_range 生成带灵活 freq 字符串的 DatetimeIndex 序列:'D'(日)、'H'(时)、'MS'(月初)、'W-MON'(每周一)、'Q'(季)。bdate_range 跳过周末。Period 表示区间(整月、整季)而非瞬时——报告期有用。freq 别名强大:'2D' = 每 2 天。

pandas
# Generate a sequence of dates
pd.date_range("2024-01-01", "2024-01-10")           # daily
pd.date_range("2024-01-01", periods=10)             # 10 days
pd.date_range("2024-01-01", periods=10, freq="H")   # hourly
pd.date_range("2024-01-01", "2024-12-31", freq="MS") # month start
pd.date_range("2024-01-01", "2024-12-31", freq="W-MON") # weekly Mondays

# Business days only
pd.bdate_range("2024-01-01", periods=10)

# Period (represents an interval, like a month)
p = pd.Period("2024-06", freq="M")
p.to_timestamp()    # convert to the month's start
p.start_time, p.end_time

# Period range
pd.period_range("2024-01", "2024-12", freq="M")

重采样与频率转换

resample 是时间序列的 groupby——降采样用聚合(W/M/Q 求和与均值),升采样用填充(ffill、interpolate)。resample 需 DatetimeIndex。'label' 和 'closed' 参数控制箱边界属于左还是右周期——对周/月聚合避免差一错误重要。

pandas
ts = pd.Series(range(100), index=pd.date_range("2024-01-01", periods=100, freq="D"))

# Downsample (more frequent -> less frequent): use agg
ts.resample("W").sum()       # weekly sum
ts.resample("M").mean()      # monthly mean
ts.resample("QE").max()      # quarterly max

# Upsample (less -> more frequent): need fill
ts.resample("H").asfreq()            # hourly, NaN for gaps
ts.resample("H").ffill()             # forward fill
ts.resample("H").interpolate()       # linear interp

# Custom aggregation
ts.resample("W").agg(["mean", "std", "count"])

# Resample with multiple columns
df.resample("M").agg({"sales": "sum", "visits": "mean"})

# Use 'label' and 'closed' to control bin edges
ts.resample("M", label="right", closed="right").sum()

移位与滞后

shift、diff 和 pct_change 是时间序列特征工程的基础。shift(1) 创建滞后(昨天值对齐今天行)——自回归特征完美。shift(-1) 偷看未来(用于建下一步目标)。diff 和 pct_change 衡量变化。用这些前始终按时间排序,并留意边界 NaN。

pandas
# Shift values forward/backward (for time-series features)
ts.shift(1)          # move values down by 1 (yesterday's value)
ts.shift(-1)         # move up by 1 (tomorrow's value)

# Difference (today - yesterday)
ts.diff()            # first difference
ts.diff(7)           # week-over-week difference

# Percentage change
ts.pct_change()
ts.pct_change(periods=7)  # 7-day pct change

# Lag features for ML
df["lag_1"]  = df["sales"].shift(1)
df["lag_7"]  = df["sales"].shift(7)
df["roll_7"] = df["sales"].rolling(7).mean()
df["target_next"] = df["sales"].shift(-1)  # next-day target

# Shift by a time frequency
ts.shift(1, freq="D")   # shift the INDEX by 1 day

滚动与扩展窗口

rolling 在滑动窗口上计算统计量——移动平均、波动率等。min_periods 让部分窗口计算(否则前 N-1 个为 NaN)。ewm 给指数加权移动平均(近期点权重更高——金融常见)。expanding 从开始增长(累计统计)。基于时间的窗口('7D')正确处理不规则采样,不像行计数窗口。

pandas
s = pd.Series([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], dtype=float)

# Rolling window (fixed-size sliding window)
s.rolling(3).mean()        # 3-period moving average
s.rolling(7).std()
s.rolling(3, min_periods=1).mean()  # allow partial windows

# Custom aggregation
s.rolling(3).apply(lambda x: x.max() - x.min())

# Exponentially weighted (decaying weights)
s.ewm(span=7).mean()       # EWMA — recent data weighted more
s.ewm(alpha=0.3).mean()

# Expanding window (grows from start)
s.expanding().mean()       # cumulative mean
s.expanding().max()        # running max

# Window with time-based offset (needs DatetimeIndex)
ts.rolling("7D").mean()    # 7-day window, not 7 rows
12

分类数据

创建分类

category dtype 把字符串存为整数码加查找表——对低基数列省大量内存(百万行 5 个唯一值的城市列)。有序分类支持比较(size > 'M')且按类别顺序排序,非字母。对已知小值集列用 category。

pandas
# Convert to category dtype
df["city"] = df["city"].astype("category")

# Create with explicit categories
s = pd.Categorical(["a", "b", "a", "c"],
                    categories=["a", "b", "c", "d"])

# Ordered categorical (for sorting & comparisons)
size = pd.Categorical(["M", "L", "S", "XL"],
                       categories=["S", "M", "L", "XL"],
                       ordered=True)
size > "M"  # works because it's ordered

# Memory savings
df["city"].memory_usage(deep=True)              # object
df["city"].astype("category").memory_usage(deep=True)  # much less

.cat 访问器

.cat 访问器操作分类元数据。remove_unused_categories 把类别列表裁剪到实际值——筛选后多类别变空时重要。rename_categories 改标签不改底层码。reorder_categories 改排序顺序。这些都返回新 Series;重新赋值以应用。

pandas
s = pd.Series(["a", "b", "a", "c"], dtype="category")

# Inspect categories
s.cat.categories        # Index(['a', 'b', 'c'])
s.cat.codes             # integer codes
s.value_counts()

# Add / remove categories
s = s.cat.add_categories(["d"])
s = s.cat.remove_categories(["d"])     # values in 'd' become NaN
s = s.cat.remove_unused_categories()

# Rename categories
s = s.cat.rename_categories({"a": "alpha", "b": "beta"})

# Reorder categories
s = s.cat.reorder_categories(["c", "b", "a"])

# Set / unset ordering
s = s.cat.as_ordered()
s = s.cat.as_unordered()

分类操作

分类操作通常比 object(字符串)操作对 groupby 和 value_counts 快。合并两个分类列时,先用 set_categories 对齐类别集——不匹配类别导致慢合并和静默 NaN。当 category 开销不值时(如自由文本列)用 astype(str) 转回 string/object。

pandas
# Operations preserve categorical dtype where possible
s = pd.Series(["low", "high", "med", "low"], dtype="category")
s.value_counts()

# Comparison on ordered categoricals
size = pd.Series(["M", "L", "S"], dtype="category")
size = size.cat.as_ordered().cat.reorder_categories(["S","M","L","XL"])
size > "M"   # boolean Series

# Groupby works as expected
df.groupby("city")["score"].mean()

# Converting back
df["city"] = df["city"].astype(str)         # to string
df["city"] = df["city"].astype("object")    # to object

# Beware: merges between categoricals can be slow if categories differ
# Align categories before merging:
cats = sorted(set(df1["city"].cat.categories) |
              set(df2["city"].cat.categories))
df1["city"] = df1["city"].cat.set_categories(cats)
df2["city"] = df2["city"].cat.set_categories(cats)

内存优化

加载大数据时内存优化重要:低基数字符串转 category(省 10-100 倍),数值降序(int64->int8 省小范围整数,float64->float32)。'string' dtype(pandas 1.0+)比 object 省内存且比 object 更好处理 NaN。经验法则:nunique/len < 0.5 时 category 值得。

pandas
# Check current memory
df.info(memory_usage="deep")

# Convert object columns to category when cardinality is low
for col in df.select_dtypes(include="object"):
    if df[col].nunique() / len(df) < 0.5:  # <50% unique
        df[col] = df[col].astype("category")

# Downcast numeric columns
df["age"] = pd.to_numeric(df["age"], downcast="integer")
df["score"] = pd.to_numeric(df["score"], downcast="float")

# Use string dtype (pandas 1.0+) instead of object
df["name"] = df["name"].astype("string")

# Compare memory before/after
before = df.memory_usage(deep=True).sum()
# ... optimize ...
after = df.memory_usage(deep=True).sum()
print(f"Reduced from {before/1e6:.1f}MB to {after/1e6:.1f}MB")

哑变量(独热编码)

get_dummies 把分类列独热编码成 0/1 指示列——线性模型不能处理分类时必不可少。drop_first 避免哑变量陷阱(多重共线性)用于线性回归。ML 管道用 sklearn 的 OneHotEncoder(通过 handle_unknown='ignore' 处理测试时未见类别)优于 get_dummies。

pandas
# One-hot encode categoricals
pd.get_dummies(df["city"])
pd.get_dummies(df, columns=["city", "pass"])  # encode multiple

# Drop first to avoid multicollinearity (for linear models)
pd.get_dummies(df["city"], drop_first=True)

# With a prefix
pd.get_dummies(df["city"], prefix="city")

# From categorical with all categories
pd.get_dummies(df["city"].astype("category"))

# Alternatively, sklearn's OneHotEncoder (for ML pipelines)
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
X_ohe = ohe.fit_transform(df[["city"]])
13

性能与优化

向量化(避免 iterrows)

iterrows 是 pandas 头号性能罪——比向量化运算慢 100-1000 倍。始终优先向量化列运算(df['a'] + df['b'])、.str 字符串、np.where if-else。确需循环时 itertuples 比 iterrows 快得多(具名元组 vs Series)。复杂逻辑考虑 numba 或 cython,或用 .to_numpy() 提取 numpy。

pandas
# BAD: iterrows is extremely slow
for idx, row in df.iterrows():
    df.loc[idx, "x"] = row["a"] + row["b"]

# GOOD: vectorized operations
df["x"] = df["a"] + df["b"]

# GOOD: .str accessor for strings
df["name"] = df["name"].str.upper()

# GOOD: np.where for conditionals
df["cat"] = np.where(df["a"] > 0, "pos", "neg")

# If you must iterate (rare), use itertuples (faster than iterrows)
for row in df.itertuples():
    process(row.a, row.b)

# Or .to_numpy() / .values for raw array loops
for row in df[["a", "b"]].to_numpy():
    ...

# Benchmark
%timeit df["a"] + df["b"]
%timeit df.apply(lambda r: r["a"] + r["b"], axis=1)

eval 与 query

eval 和 query 解析字符串表达式并用 numexpr 求值,大 DataFrame 上可快 2-4 倍,因为避免创建中间数组且用 C 级求值。小数据收益缩小(解析开销)。对大帧复杂表达式有用——但始终基准测试,因不总更快。

pandas
# eval: evaluate string expressions (uses numexpr, faster on big data)
df.eval("total = a + b + c", inplace=True)
df.eval("ratio = a / (b + c)")
df.eval("c = a > b")

# Multiple operations in one eval (efficient)
df.eval("""
total = a + b
ratio = a / b
flag = a > 10
""", inplace=True)

# query: boolean filter via string (same engine)
df.query("a > 0 and b < 10")

# Local variables with @
threshold = 5
df.query("a > @threshold")

# eval is faster than Python arithmetic on large frames
# because it avoids intermediate arrays

高效 dtype

选对 dtype 是内存最易得的大收益:数值降序(int64->int8 省 8 倍),低基数字符串用 category(省 10-100 倍),用 'string' 替代 object。可空 Int64(大写 I)处理含 NaN 的整数列——常规 int64 不能有 NaN,所以这类列默认 float64。Sparse dtype 对几乎全零数据有帮助。

pandas
# Downcast numerics
df["age"] = pd.to_numeric(df["age"], downcast="unsigned")  # 0-255
df["score"] = pd.to_numeric(df["score"], downcast="float")

# Use category for low-cardinality strings
df["city"] = df["city"].astype("category")

# Use 'string' dtype instead of object
df["name"] = df["name"].astype("string")

# Use Nullable integers (Int64 with capital I) for ints with NaN
df["count"] = df["count"].astype("Int64")  # supports NaN

# Sparse for mostly-zero data
s = pd.Series([0, 0, 0, 1, 0, 0]).astype("Sparse[int]")

# Compare memory
df["city"].astype("object").memory_usage(deep=True)
df["city"].astype("category").memory_usage(deep=True)

分块处理

read_csv(chunksize=...) 返回迭代器——每块是该行数的 DataFrame,让你处理大于内存的文件。每块聚合,再合并部分结果。SQL 用 chunksize 用服务端游标。这是无 Spark 处理 GB 级数据的最简方式;真正大数据考虑 Dask 或 Polars。

pandas
# Process a large CSV in chunks (low memory)
chunk_iter = pd.read_csv("huge.csv", chunksize=100_000)

results = []
for chunk in chunk_iter:
    # process each chunk (filter, aggregate, etc.)
    result = chunk.groupby("city")["sales"].sum()
    results.append(result)

# Combine chunk results
total = pd.concat(results).groupby(level=0).sum()

# Or use the iterator directly
for i, chunk in enumerate(pd.read_csv("huge.csv", chunksize=50000)):
    chunk.to_parquet(f"part_{i:04d}.parquet")

# For SQL, use server-side cursors
chunks = pd.read_sql("SELECT * FROM big_table", engine,
                     chunksize=100000)

性能分析与基准测试

优化前始终基准测试——%timeit 快速比较方法。ydata-profiling(原 pandas-profiling)生成完整 HTML 报告(统计、相关、缺失)——EDA 极有价值。留意 SettingWithCopyWarning:它信号你的赋值可能不持久,是静默 bug 常见源。开发时设 mode.chained_assignment='raise' 把它们作为错误捕获。

pandas
# Time an operation
%timeit df["a"] + df["b"]

# Line profiling (install line_profiler)
# %load_ext line_profiler
# %lprun -f my_function my_function(df)

# Memory profiling (install memory_profiler)
# %load_ext memory_profiler
# %memit df.groupby("city").sum()

# pandas profiling report (install ydata-profiling)
# from ydata_profiling import ProfileReport
# ProfileReport(df).to_file("report.html")

# Quick stats
df.info(memory_usage="deep")
df.describe(include="all")

# Find slow ops
import pandas as pd
pd.set_option("mode.chained_assignment", "warn")  # catch copy bugs
14

数据库与 SQL

从 SQL 读取

SQLAlchemy 引擎处理连接池和方言差异——用它们而非原始 DBAPI 连接。用户提供的值始终用参数化查询(params=)防 SQL 注入。chunksize 流式处理大表分批。复杂查询用 SQL 写(数据库为之优化),让 pandas 处理结果。

pandas
from sqlalchemy import create_engine
import pandas as pd

# Create an engine (connection pool)
engine = create_engine("sqlite:///my.db")
engine = create_engine("postgresql://user:pass@host:5432/db")
engine = create_engine("mysql+pymysql://user:pass@host/db")

# Read a whole table
df = pd.read_sql("users", engine)

# Read a query
df = pd.read_sql("SELECT * FROM users WHERE age > 18", engine)

# Parameterized queries (safe from SQL injection)
df = pd.read_sql(
    "SELECT * FROM users WHERE city = %s",
    engine,
    params=("NYC",),
)

# Chunked reads for large tables
for chunk in pd.read_sql("SELECT * FROM big", engine, chunksize=100000):
    process(chunk)

写入 SQL

if_exists='replace' 删并重建表;'append' 加行;'fail'(默认)表存在时报错。method='multi' 把行批成一条 INSERT——比默认每行一条快得多。很大写入时 chunksize=10000 平衡内存和往返。除非索引是真数据,否则始终传 index=False。

pandas
# Write a DataFrame to a SQL table
df.to_sql("users", engine, if_exists="replace", index=False)
df.to_sql("users", engine, if_exists="append", index=False)  # add rows

# Specify schema and dtype
from sqlalchemy.types import VARCHAR, INTEGER
df.to_sql("users", engine, if_exists="replace",
          dtype={"name": VARCHAR(100), "age": INTEGER()})

# Write in chunks (for large DataFrames)
df.to_sql("users", engine, if_exists="append",
          chunksize=10000, method="multi")

# method='multi' uses a single INSERT with multiple rows (faster)
# For PostgreSQL, method='multi' with chunksize ~10000 is optimal

# Don't write the index unless it's meaningful data
# index=False avoids a spurious 'index' column

pandas 中的 SQL 风格操作

大多数 SQL 操作映射到 pandas:SELECT 是列选择,WHERE 是布尔索引,GROUP BY 是 groupby,JOIN 是 merge,UNION 是 concat+drop_duplicates,CASE WHEN 是 np.where。知道这些映射让你选对工具:复杂连接/聚合用 SQL(让 DB 优化),迭代分析和绘图用 pandas。

pandas
# SELECT cols
df[["name", "age"]]
# WHERE
df[df["age"] > 18]
# GROUP BY + aggregations
df.groupby("city")["age"].mean()
# ORDER BY
df.sort_values("age", ascending=False)
# LIMIT
df.head(10)
# DISTINCT
df["city"].unique()
df.drop_duplicates(subset=["city"])
# JOIN
pd.merge(df1, df2, on="id", how="inner")
# UNION
pd.concat([df1, df2]).drop_duplicates()
# CASE WHEN
np.where(df["age"] >= 18, "adult", "minor")
# HAVING
g = df.groupby("city").filter(lambda g: g["age"].mean() > 30)

用 text() 执行 SQL

SQLAlchemy 2.0 对原始 SQL 字符串要求 text()——这是区分可信 SQL 与不可信参数的安全措施。engine.begin() 开启成功自动提交、异常回滚的事务——写操作始终用它。用 :name 和 params 字典绑定参数防注入。

pandas
from sqlalchemy import text

# Use text() for explicit SQL (recommended over raw strings)
query = text("SELECT city, COUNT(*) as n FROM users GROUP BY city")
df = pd.read_sql(query, engine)

# Bind parameters safely
query = text("SELECT * FROM users WHERE age > :min_age AND city = :city")
df = pd.read_sql(query, engine,
                 params={"min_age": 18, "city": "NYC"})

# Execute DDL or DML (non-SELECT)
with engine.begin() as conn:  # transaction
    conn.execute(text("CREATE INDEX idx_city ON users(city)"))
    conn.execute(text("DELETE FROM users WHERE id = :id"),
                 {"id": 999})

Parquet vs CSV vs SQL

Parquet 是存储最佳默认——列式、压缩、保 dtype(CSV 丢 dtype 且每次加载强制重新推断)。Feather 是短期缓存最快(Apache Arrow 格式,无序列化开销)。Pickle 方便但跨 Python/pandas 版本易坏。数据共享且需服务端查询时用 SQL。

pandas
# CSV: human-readable, slow, loses dtypes, large
df.to_csv("data.csv", index=False)
df = pd.read_csv("data.csv")  # re-infers dtypes (slow, error-prone)

# Parquet: binary, fast, preserves dtypes, compressed (best for storage)
df.to_parquet("data.parquet")
df = pd.read_parquet("data.parquet")  # dtypes preserved exactly

# Feather: fastest for short-lived storage (Apache Arrow)
df.to_feather("data.feather")  # requires a RangeIndex
df = pd.read_feather("data.feather")

# Pickle: Python-specific, fast, but version-fragile
df.to_pickle("data.pkl")
df = pd.read_pickle("data.pkl")

# SQL: shared, queryable, but slower for bulk reads
df.to_sql("table", engine)
df = pd.read_sql("SELECT * FROM table", engine)

# Rule of thumb:
# - CSV for human exchange
# - Parquet for storage/archival (best default)
# - Feather for fast cache between Python processes
# - SQL for shared, queryable data
15

时间序列分析

设置 DatetimeIndex

pandas 时间序列工作从 DatetimeIndex 开始——把日期列设为索引并排序。然后可用字符串切片(df.loc['2024-06'] 取整个六月)及 resample/rolling/shift。部分字符串索引极方便。设置索引后始终排序;许多时间序列方法要求有序索引。

pandas
# Ensure the date column is datetime, then set as index
df["date"] = pd.to_datetime(df["date"])
df = df.set_index("date").sort_index()

# Now you can slice by date strings
df.loc["2024"]                 # all of 2024
df.loc["2024-06"]              # June 2024
df.loc["2024-06-01":"2024-06-15"]  # date range (inclusive)

# Partial string indexing on Series
s = df["value"]
s["2024-06-15"]

# Filter with date strings
df[df.index >= "2024-01-01"]

# A DatetimeIndex unlocks resample, rolling, shift, etc.

重采样实践

resample 是时间序列 groupby——'MS'(月初)、'ME'(月末)、'QE'(季末)、'W'(周)。与 groupby 组合做每类别重采样(如每城市周销售)。asfreq 为缺失时段加行(填 NaN),对暴露不规则数据间隙有用。last() 聚合取每期最后值。

pandas
# Daily -> monthly revenue
daily.resample("MS").sum()

# Common frequencies:
# D=day, B=business day, W=weekly, MS=month start, ME=month end,
# QS=quarter start, YE=year end, H=hour, T/min=minute, S=second

# Multiple aggregations
daily.resample("ME").agg({"sales": "sum", "users": "mean"})

# Resample per group
df.groupby("city").resample("W")["sales"].sum()

# Handle missing periods (fill gaps in the timeline)
daily.asfreq("D")           # add NaN rows for missing days
daily.asfreq("D").ffill()   # fill them

# Quarter-end reporting
daily.resample("QE").last()  # last value of each quarter

时区

tz_localize 给朴素时间戳分配时区(不改值);tz_convert 转其他时区(改显示值)。两者易混。始终先 localize 再 convert。DST 转换产生歧义/缺失时间——用 ambiguous='infer'(对有序数据)或 'NaT' 处理。

pandas
# Localize naive timestamps (assign a timezone)
s = pd.Series(pd.date_range("2024-01-01", periods=3, freq="H"))
s_localized = s.dt.tz_localize("UTC")
s_localized = s.dt.tz_localize("America/New_York")

# Convert between timezones
s_utc = s.dt.tz_localize("UTC")
s_est = s_utc.dt.tz_convert("US/Eastern")
s_hk  = s_utc.dt.tz_convert("Asia/Hong_Kong")

# On a DatetimeIndex
idx = pd.date_range("2024-01-01", periods=3, freq="H", tz="UTC")
idx = idx.tz_convert("Asia/Shanghai")

# Remove timezone info
s_naive = s_utc.dt.tz_localize(None)

# Ambiguous times (DST fall-back): use 'infer' or 'NaT'
s.dt.tz_localize("US/Eastern", ambiguous="infer")

窗口函数

基于时间的滚动窗口('7D')正确处理不规则采样数据,不像行计数窗口(rolling(7))。ewm(指数加权)近期观测权重更高——金融平滑标配。groupby + rolling 计算每组窗口(如每股票 7 日移动平均)。groupby+rolling 产生的 MultiIndex 用 reset_index 清理。

pandas
# Rolling window with time offset
ts.rolling("7D").mean()      # 7-day window (handles irregular sampling)

# Rolling with custom function
ts.rolling("30D").apply(lambda x: np.quantile(x, 0.95))

# Expanding window (cumulative)
ts.expanding(min_periods=10).mean()

# Exponentially weighted
ts.ewm(span=20).mean()       # like a 20-period EWMA
ts.ewm(halflife=10).mean()

# Window per group
df.groupby("city")["sales"].rolling("7D").mean().reset_index(level=0, drop=True)

# Compare rolling vs ewm for smoothing
# ewm weights recent points more — better for fast-changing series

季节性与分解

seasonal_decompose 把时间序列拆成趋势、季节和残差分量——理解结构和去季节化有用。period 是周期长度(月数据年度季节性用 12)。在 index.month 或 index.dayofweek 上 groupby 揭示季节模式。autocorrelation_plot 显示序列与自身滞后的相关性。

pandas
# Seasonal decomposition (needs statsmodels)
from statsmodels.tsa.seasonal import seasonal_decompose

# Additive model: y = trend + seasonal + residual
result = seasonal_decompose(ts, model="additive", period=12)
result.plot()
result.trend, result.seasonal, result.resid

# Multiplicative model: y = trend * seasonal * residual
result = seasonal_decompose(ts, model="multiplicative", period=12)

# Seasonal plots
ts.groupby(ts.index.month).mean()   # average by month
ts.groupby(ts.index.dayofweek).mean()  # by weekday

# Lag plot (autocorrelation check)
pd.plotting.lag_plot(ts, lag=1)
pd.plotting.autocorrelation_plot(ts)
16

可视化

基础图表

pandas 绘图建在 matplotlib 上——.plot 访问器为 EDA 给快速图表。plot() 默认线图(时间序列好),plot.bar() 类别计数,plot.hist() 分布,plot.scatter() 关系。出版级图表转 seaborn 或 plotly,但分析期间快速检视 pandas 绘图无与伦比。

pandas
import matplotlib.pyplot as plt

# Line plot (default for time series)
df["score"].plot()
df.plot(x="date", y="score")

# Bar chart
df["city"].value_counts().plot.bar()
df["city"].value_counts().plot.barh()  # horizontal

# Histogram
df["age"].plot.hist(bins=20)
df[["age", "score"]].plot.hist(alpha=0.5, bins=20)

# Scatter
df.plot.scatter(x="age", y="score", c="pass", colormap="viridis")

# Box plot (per group)
df.boxplot(column="score", by="city")

# Area, density, pie
df["city"].value_counts().plot.pie(autopct="%1.1f%%")
df["score"].plot.kde()  # kernel density estimate

plt.tight_layout(); plt.show()

用 Seaborn 绘图

Seaborn 在统计可视化上闪耀:小提琴图、配对图(散点矩阵)、热力图、带置信区间的回归线。它优雅处理『按颜色分组』(hue),matplotlib 中很冗长。pairplot 是扫描数据集所有成对关系最快方式。始终传 DataFrame 用列名——seaborn 的 API 为整洁数据设计。

pandas
import seaborn as sns
import matplotlib.pyplot as plt

# Statistical plots that pandas can't easily do
sns.histplot(df["score", kde=True])
sns.boxplot(data=df, x="city", y="score")
sns.violinplot(data=df, x="city", y="score")

# Relationships
sns.scatterplot(data=df, x="age", y="score", hue="city")
sns.pairplot(df[["age", "score", "pass"]], hue="pass")

# Correlation heatmap
sns.heatmap(df.corr(numeric_only=True), annot=True, cmap="coolwarm")

# Regression plot
sns.regplot(data=df, x="age", y="score")

# Facet grid (small multiples)
g = sns.FacetGrid(df, col="city", hue="pass")
g.map(sns.histplot, "score")

plt.tight_layout(); plt.show()

用 Plotly 交互绘图

Plotly Express 生成交互图表(悬停提示、缩放、平移)——仪表盘和与非技术利益相关者分享洞察绝佳。API 镜像 seaborn(传 DataFrame,列映射到美学)。write_html 保存自包含交互 HTML 文件。百万点纯性能考虑 datashader;精美度 Plotly 优秀。

pandas
import plotly.express as px

# Interactive scatter (hover, zoom, pan)
fig = px.scatter(df, x="age", y="score", color="city",
                 hover_data=["name"], title="Score vs Age")
fig.show()

# Line chart
fig = px.line(df, x="date", y="score", color="city")

# Bar
fig = px.bar(df, x="city", y="score", color="pass", barmode="group")

# Histogram
fig = px.histogram(df, x="score", color="city", marginal="box")

# Faceted
fig = px.scatter(df, x="age", y="score", facet_col="city")

# 3D
fig = px.scatter_3d(df, x="age", y="score", z="hours", color="city")

# Save as HTML (interactive, no JS needed to view)
fig.write_html("plot.html")

子图与布局

plt.subplots 创建轴网格;绘图时传给 ax=。sharex/sharey 跨子图对齐轴(比较尺度有用)。secondary_y 在不同 y 轴画第二序列(如价格和成交量)。tight_layout 防标签重叠。复杂布局 gridspec 对子图尺寸提供更细控制。

pandas
import matplotlib.pyplot as plt

# pandas plot on specific axes
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
df["age"].plot.hist(ax=axes[0, 0], title="Age")
df["score"].plot.hist(ax=axes[0, 1], title="Score")
df["city"].value_counts().plot.bar(ax=axes[1, 0], title="City")
df.plot.scatter(x="age", y="score", ax=axes[1, 1], title="Age vs Score")
plt.tight_layout(); plt.show()

# Shared x-axis
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(10, 6))
df["score"].plot(ax=ax1)
df["age"].plot(ax=ax2)

# Secondary y-axis
ax = df["score"].plot()
df["age"].plot(ax=ax, secondary_y=True)
ax.set_ylabel("Score"); ax.right_ax.set_ylabel("Age")

样式化 DataFrame

Styler 对象自定义 DataFrame 在 Jupyter 和 HTML 中渲染——渐变、单元格内条形图、条件格式让表格一眼可读。format 控制数字显示(小数、百分比)。样式在显示时计算(不改数据)且可导出 HTML 用于报告。applymap 按单元格应用函数;apply 按行/列。

pandas
# Style a DataFrame for HTML/Jupyter display
styled = df.style.highlight_max(color="lightgreen")
styled = df.style.highlight_min(color="lightcoral")
styled = df.style.background_gradient(cmap="Blues", subset=["score"])

# Bar charts inside cells
df.style.bar(subset=["score"], color="#5fba7d")

# Format numbers
df.style.format({"score": "{:.2f}", "age": "{:.0f}"})
df.style.format({"score": "{:.2%}"})  # as percentage

# Conditional formatting
def color_neg_red(val):
    color = "red" if val < 0 else "black"
    return f"color: {color}"
df.style.applymap(color_neg_red, subset=["change"])

# Combine styles
(df.style
   .format({"score": "{:.2f}"})
   .background_gradient(cmap="RdYlGn", subset=["score"])
   .set_caption("Student Scores"))

# Export to HTML
styled.to_html("table.html")
17

导出与格式化

格式化显示

显示选项控制 pandas 在控制台/Jupyter 打印方式——增 max_rows/max_columns 看更多,set float_format 一致小数。option_context 是临时改设置(之后恢复)的上下文管理器,适合一次性检视不全局改环境。常用设置放启动脚本。

pandas
# Global display options
pd.set_option("display.max_rows", 100)
pd.set_option("display.max_columns", 50)
pd.set_option("display.width", 1000)
pd.set_option("display.max_colwidth", 100)
pd.set_option("display.float_format", "{:.2f}".format)

# Reset to defaults
pd.reset_option("display.max_rows")

# Float formatting only for one operation
with pd.option_context("display.float_format", "{:.2f}".format):
    print(df)

# Temporarily show more rows
with pd.option_context("display.max_rows", None):
    print(df.head(100))

导出多格式

to_markdown 生成 README 和文档的 GitHub 就绪表格。to_html 嵌网页;to_latex 学术论文。to_dict(orient='records') 转 API 的字典列表(JSON 式)。每格式有选项——探索文档。Excel 用 engine='openpyxl' 支持格式公式、条件格式和图表。

pandas
# CSV
df.to_csv("out.csv", index=False)
df.to_csv("out.tsv", sep="\t", index=False)

# Excel with formatting (needs openpyxl)
with pd.ExcelWriter("out.xlsx", engine="openpyxl") as w:
    df.to_excel(w, sheet_name="data", index=False)
    summary.to_excel(w, sheet_name="summary")

# Markdown (great for docs/reports)
print(df.to_markdown(index=False))
df.to_markdown("table.md", index=False)

# HTML
df.to_html("table.html", index=False, classes="table")

# LaTeX
print(df.to_latex(index=False, caption="Results"))

# JSON
df.to_json("out.json", orient="records", indent=2, date_format="iso")

# Dict / records
df.to_dict(orient="records")  # list of dicts
df.to_dict(orient="list")     # dict of lists

剪贴板与字符串

to_clipboard(excel=True) 复制制表符分隔表,直接粘贴进 Excel/Google 表格——Python 与电子表格间移数据极方便。read_clipboard 是反向:从网页或电子表格复制表,无需存文件即可读入 DataFrame。to_string 给日志的纯文本表示。

pandas
# Copy a DataFrame to the clipboard (paste into Excel/Sheets)
df.to_clipboard(index=False, excel=True)

# Read from clipboard (paste a table from a spreadsheet/webpage)
df = pd.read_clipboard()

# Convert to string with formatting
print(df.to_string(index=False))
print(df.to_string(max_rows=10))

# Pretty-print in Jupyter
from IPython.display import display
display(df.head(10))

# Render a Series as a string
print(s.to_string())

# Quick peek at value (useful in debugging)
df.head().to_dict("records")

Excel 格式化

openpyxl 让你编程样式化 Excel 输出——粗体表头、列宽、冻结窗格、条件格式、公式。写入后通过 writer.book 访问工作簿,writer.sheets 访问表。自动调列宽需手动计算(Excel 文件写入时不自动)。冻结窗格滚动时保持表头可见。

pandas
from openpyxl.styles import Font, PatternFill
from openpyxl.utils import get_column_letter

# Write with conditional formatting
with pd.ExcelWriter("report.xlsx", engine="openpyxl") as writer:
    df.to_excel(writer, sheet_name="Data", index=False)

    # Access the workbook to style
    wb = writer.book
    ws = writer.sheets["Data"]

    # Bold header
    for cell in ws[1]:
        cell.font = Font(bold=True, color="FFFFFF")
        cell.fill = PatternFill("solid", fgColor="4472C4")

    # Auto-size columns
    for col in ws.columns:
        max_len = max(len(str(c.value)) for c in col if c.value is not None)
        ws.column_dimensions[get_column_letter(col[0].column)].width = max_len + 2

    # Freeze header row
    ws.freeze_panes = "A2"

可复现 Notebook

标准化 notebook 头部(导入、选项、种子、版本检查)使分析可复现且省调试时间。pd.show_versions() 报告 pandas/numpy/python 版本——贴进 bug 报告。check_df 助手打印形状、dtype、head 和缺失,标准化对任何新数据集的第一眼。只抑制特定警告类别,绝不全部。

pandas
# At the top of every analysis notebook
import pandas as pd
import numpy as np
pd.set_option("display.max_columns", None)
pd.set_option("display.float_format", "{:.4f}".format)

# Set a seed for reproducibility
np.random.seed(42)

# Show versions for reproducibility
pd.show_versions()

# Common imports grouped
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid")

# Suppress chained-assignment warnings cleanly
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

# Validate data on load
def check_df(df, name="df"):
    print(f"=== {name} ===")
    print(f"Shape: {df.shape}")
    print(df.dtypes)
    print(df.head())
    print(df.isnull().sum())
check_df(df)
18

常见陷阱

SettingWithCopyWarning

SettingWithCopyWarning 是 pandas 告诉你赋值可能不持久——链式索引(df[mask]['col']=x)可能修改临时副本。修复始终是 .loc[row_mask, col_name] = value 做条件赋值。子集后修改时用 .copy() 显式。开发中把警告设 'raise' 把这些 bug 作错误捕获。

pandas
# BAD: chained indexing — may not modify df
df[df["age"] > 30]["city"] = "NYC"   # SettingWithCopyWarning

# GOOD: single .loc assignment
df.loc[df["age"] > 30, "city"] = "NYC"

# When subsetting, be explicit about copy vs view
subset = df[df["age"] > 30].copy()   # explicit copy
subset["city"] = "NYC"               # safe — modifies subset only

# Detect the warning
pd.set_option("mode.chained_assignment", "warn")   # default
pd.set_option("mode.chained_assignment", "raise")  # error (dev mode)
pd.set_option("mode.chained_assignment", None)     # suppress

# Rule: if you're going to modify, use .loc or .copy() first

索引对齐意外

索引对齐是 pandas 超级能力也是最大坑。把 Series 赋给列时,pandas 按索引匹配,非位置——所以 Series 索引不同时得 NaN 或错值。用 .values 按位置赋值,或先 reset_index 对齐索引。此 bug 静默(无警告)且是最常见 pandas 错误之一。

pandas
# Adding Series aligns on index — NaN where they don't match
s1 = pd.Series([1, 2, 3], index=["a", "b", "c"])
s2 = pd.Series([10, 20, 30], index=["b", "c", "d"])
s1 + s2
# a     NaN
# b    12.0
# c    33.0
# d     NaN

# When assigning a Series to a column, alignment happens by index
df["new"] = some_series   # rows matched by INDEX, not position!

# To force positional alignment, use .values
df["new"] = some_series.values   # assign by position

# After sorting/resetting, indexes may not match
df1 = df.sort_values("age")
df1["rank"] = range(len(df1))   # WRONG if index isn't 0..n-1
df1["rank"] = np.arange(len(df1))  # also WRONG — aligns on index
df1["rank"] = pd.Series(range(len(df1)), index=df1.index)  # correct
# Or simply:
df1 = df1.reset_index(drop=True)
df1["rank"] = range(len(df1))

inplace vs 重新赋值

pandas 社区正远离 inplace=True 转向函数式重新赋值——它启用方法链(可读管道)且更可预测。inplace 罕省内存(pandas 常仍复制)。链式风格(df.method().method().method())从上到下读是现代惯用法。仅当测得真实内存收益时用 inplace。

pandas
# Two equivalent styles:
df = df.sort_values("age")        # reassign (returns new df)
df.sort_values("age", inplace=True)  # modify in place

# Most pandas methods return a new object (functional style).
# inplace=True modifies the object — but:
# 1. It's being deprecated in some methods
# 2. It can prevent method chaining
# 3. It sometimes still makes a copy under the hood

# Prefer functional style for chaining:
(df
   .sort_values("age")
   .reset_index(drop=True)
   .rename(columns={"old": "new"}))

# inplace is fine for memory savings on huge DataFrames,
# but rarely matters otherwise.

可变默认参数

逐行增长 DataFrame(df.append 或循环中 pd.concat)是 O(n²)——每次追加复制整帧。始终在列表中收集行,然后一次构建 DataFrame。Python 可变默认参数(列表、字典)同理——跨调用共享,是经典 Python bug。用 None 作哨兵并在函数内初始化。

pandas
# BAD: default mutable args are shared across calls
def add_row(df, new_data=[]):   # NEVER use mutable defaults
    new_data.append(1)
    return df

# GOOD: use None
def add_row(df, new_data=None):
    if new_data is None:
        new_data = []
    ...

# Building a DataFrame row by row (SLOW — anti-pattern)
df = pd.DataFrame()
for item in items:
    df = pd.append(df, item)  # deprecation + O(n^2)

# GOOD: build a list of records, then construct once
records = []
for item in items:
    records.append({"name": item.name, "value": item.value})
df = pd.DataFrame(records)

# Or use from_records
df = pd.DataFrame.from_records(items, columns=["name", "value"])

NaN 传播

NaN 永不等于自身(NaN == NaN 为 False)——用 isna() 检测缺失值,绝不用 ==。聚合默认跳过 NaN(sum、mean),但传 skipna=False 传播它。含 NaN 的整数列被提升为 float64(因 numpy 整数不能持 NaN);用可空 'Int64' dtype(大写 I)保留含 NaN 的整数。

pandas
# NaN propagates through most operations
np.nan + 1          # nan
np.nan > 0          # False
pd.Series([1, 2, np.nan]).sum()    # 3.0 (skips NaN by default)
pd.Series([1, 2, np.nan]).mean()   # 1.5

# Some operations DON'T skip NaN
pd.Series([1, 2, np.nan]).sum(skipna=False)  # nan

# Comparisons with NaN are always False
pd.Series([1, np.nan]) == pd.Series([1, np.nan])
# [True, False]  — NaN != NaN!

# Check for NaN correctly
s.isna()            # True for NaN
s.isnull()          # alias
s == np.nan         # WRONG — always False!
s.fillna(0) == 0    # check after filling

# Integer columns with NaN become float
s = pd.Series([1, 2, None])   # becomes float64: [1.0, 2.0, NaN]
# Use nullable Int64 to keep integer with NaN
s = pd.Series([1, 2, None], dtype="Int64")
19

实战模式

EDA 模板

标准化 EDA 函数确保建模前始终检查形状、dtype、缺失、分布和重复。尽早捕获错误 dtype(数字作字符串)、隐藏缺失(哨兵值如 -999)或意外重复省下游调试数小时。每个新数据集第一步都跑这个。

pandas
import pandas as pd
import numpy as np

def eda(df, name="df"):
    print(f"=== {name} ===")
    print(f"Shape: {df.shape}")
    print("\n--- dtypes ---")
    print(df.dtypes)
    print("\n--- head ---")
    print(df.head())
    print("\n--- missing ---")
    print(df.isnull().sum())
    print("\n--- numeric summary ---")
    print(df.describe())
    print("\n--- categorical summary ---")
    print(df.describe(include="object"))
    print("\n--- duplicates ---")
    print(f"{df.duplicated().sum()} duplicate rows")

# Usage
eda(df)

# Per-column deep dive
for col in df.columns:
    print(f"\n### {col}")
    print(f"dtype: {df[col].dtype}")
    print(f"unique: {df[col].nunique()}")
    if df[col].dtype == "object":
        print(df[col].value_counts().head(10))
    else:
        print(df[col].describe())

构建清洗管道

方法链(.pipe / .assign / .dropna 管道)使清洗可复现可测试——每步是纯函数,任意点易检视。尽早把列名转 snake_case 避免引号头疼。末尾断言捕获静默失败(如所有日期解析失败)。把管道包进函数以在新数据批次复用。

pandas
def clean(df):
    """A reusable cleaning pipeline."""
    return (df
        # Standardize column names
        .rename(columns=lambda c: c.strip().lower().replace(" ", "_"))
        # Drop empty columns
        .dropna(axis=1, how="all")
        # Parse dates
        .assign(date=lambda d: pd.to_datetime(d["date"], errors="coerce"))
        # Numeric coercion
        .assign(amount=lambda d: pd.to_numeric(d["amount"], errors="coerce"))
        # Fill missing
        .fillna({"category": "unknown"})
        # Dedupe
        .drop_duplicates()
        # Reset index
        .reset_index(drop=True)
    )

df_clean = clean(df_raw)

# Verify
assert df_clean["date"].notnull().all(), "Some dates failed to parse"
assert len(df_clean) <= len(df_raw), "Cleaning should not add rows"

透视表分析

pivot_table 是分析师瑞士军刀——配 margins=True 得总计,热力图即时洞察。逆(melt)带你回整洁长格式做后续处理。『每组 Top N』模式(排序后 groupby head)在业务报告中极常见。

pandas
# Multi-dimensional analysis in one shot
pivot = pd.pivot_table(
    df,
    values="revenue",
    index=["region", "product"],
    columns=["quarter"],
    aggfunc="sum",
    fill_value=0,
    margins=True,         # add row/column totals
    margins_name="Total",
)

# Heatmap of the pivot
import seaborn as sns
sns.heatmap(pivot, annot=True, fmt=".0f", cmap="YlGnBu")

# Convert back to long
long = pivot.reset_index().melt(
    id_vars=["region", "product"],
    var_name="quarter",
    value_name="revenue",
)

# Top product per region
top = (df
    .groupby(["region", "product"])["revenue"].sum()
    .reset_index()
    .sort_values("revenue", ascending=False)
    .groupby("region").head(1))

ML 特征工程

特征工程是领域知识遇 pandas:每实体聚合(客户终身消费)、比率(月均消费)、时间特征(任期)、分箱类别。按实体 ID 把聚合特征合并回主帧。get_dummies 为线性模型独热编码。包进函数以使训练和推理时相同变换。

pandas
def add_features(df):
    """Add features for a churn model."""
    out = df.copy()

    # Time-based features
    out["tenure_years"] = out["tenure_months"] / 12
    out["is_new_customer"] = (out["tenure_months"] < 12).astype(int)

    # Aggregations per customer (need transaction history)
    # txns is a separate transactions DataFrame
    agg = txns.groupby("customer_id").agg(
        total_spend=("amount", "sum"),
        txn_count=("amount", "count"),
        avg_txn=("amount", "mean"),
        last_txn_date=("date", "max"),
    ).reset_index()
    out = out.merge(agg, on="customer_id", how="left")

    # Ratio features
    out["spend_per_month"] = out["total_spend"] / out["tenure_months"]

    # Binning
    out["age_group"] = pd.cut(out["age"], bins=[0,25,40,60,100],
                              labels=["young","adult","mid","senior"])

    # Encode categoricals
    out = pd.get_dummies(out, columns=["age_group", "plan"], drop_first=True)

    return out

df_ml = add_features(df)

读取乱 CSV

真实 CSV 乱:错编码(utf-8 失败时试 latin-1 或 cp1252)、欧洲小数逗号(decimal=',')、表头前元数据行(skiprows)、哨兵空值(na_values)。usecols 只加载所需列省宽文件内存。盲读前始终用文本编辑器检视陌生 CSV 前几行。

pandas
# Common CSV issues and fixes

# Bad encoding
df = pd.read_csv("data.csv", encoding="latin-1")  # or utf-8-sig, cp1252

# Wrong separator
df = pd.read_csv("data.tsv", sep="\t")
df = pd.read_csv("data.csv", sep=";", decimal=",")  # European format

# Skip metadata rows at the top
df = pd.read_csv("data.csv", skiprows=3)

# Multiple header rows
df = pd.read_csv("data.csv", header=[0, 1])

# Custom NA values
df = pd.read_csv("data.csv", na_values=["?", "N/A", "NULL", -999])

# Parse dates with multiple formats
df = pd.read_csv("data.csv", parse_dates=["date"], dayfirst=True)

# Only load specific columns (saves memory)
df = pd.read_csv("data.csv", usecols=["id", "date", "amount"])

# Handle quoted fields with commas/newlines
df = pd.read_csv("data.csv", quotechar='"', escapechar="\\")
20

窗口操作深入

带自定义函数的滚动

内置滚动聚合(mean、std、median、quantile)是 C 优化且快。自定义 apply 灵活但慢(每窗口 Python 回调)。加权平均或回归时用 apply 内 numpy 运算。raw=True(默认)传 numpy 数组(快);需索引时 raw=False 传 Series。

pandas
s = pd.Series(range(20), dtype=float)

# Built-in aggregations (fast, C-optimized)
s.rolling(5).mean()
s.rolling(5).std()
s.rolling(5).median()
s.rolling(5).quantile(0.75)
s.rolling(5).sum()

# Custom function (slower, but flexible)
s.rolling(5).apply(lambda x: x.max() - x.min())

# Weighted rolling average
weights = np.array([0.1, 0.2, 0.3, 0.25, 0.15])
s.rolling(5).apply(lambda x: np.dot(x, weights))

# Rolling regression (slope over window)
def slope(x):
    return np.polyfit(range(len(x)), x, 1)[0]
s.rolling(10).apply(slope)

# raw=False passes Series (with index) to the function
s.rolling(5).apply(lambda x: x.iloc[-1] / x.iloc[0], raw=False)

基于时间的窗口

基于时间的窗口('7D')正确处理不规则采样数据——包含时间范围内所有点,非固定行数。这对有间隙的真实时间序列必不可少。center=True 把窗口居中当前点(平滑用)。closed='left' 排除当前点(从『未来』算特征时避免泄露)。

pandas
# Row-count vs time-based windows
ts = pd.Series(range(100), index=pd.date_range("2024-01-01", periods=100, freq="D"))

# Row-count: exactly 7 rows (may span more than 7 days if data is irregular)
ts.rolling(7).mean()

# Time-based: all rows within the last 7 days (handles gaps)
ts.rolling("7D").mean()

# Time-based is essential for irregular time series
irregular = pd.Series([1, 2, 3],
    index=pd.to_datetime(["2024-01-01", "2024-01-05", "2024-01-15"]))
irregular.rolling("7D").sum()   # 1, 3, 3 (third is just itself, no neighbors in 7d)

# Center the window
ts.rolling("7D", center=True).mean()  # window centered on current point

# Closed controls which endpoints are included
ts.rolling("7D", closed="left").mean()  # exclude current point

扩展与 EWMA

expanding 计算从开始到当前点所有数据的统计——累计基准和运行极值有用。ewm(指数加权)近期观测权重更高——金融平滑标配(EWMA)。span 直观(类似 N 期移动平均);alpha 是原始平滑因子。EWMA 比简单移动平均对近期变化反应更快。

pandas
s = pd.Series([1, 2, 3, 4, 5, 6, 7, 8], dtype=float)

# Expanding: window grows from the start
s.expanding(min_periods=2).mean()   # cumulative mean
s.expanding().max()                 # running maximum
s.expanding().std()

# Exponentially weighted moving average
s.ewm(span=5).mean()       # span=N similar to N-period moving avg
s.ewm(alpha=0.3).mean()    # alpha = smoothing factor (0-1)
s.ewm(halflife=3).mean()   # halflife: weight halves every N periods

# EWMA volatility (finance)
returns.ewm(span=20).std()  # 20-period EWMA volatility

# Relationship: span = (2/alpha) - 1
# Higher alpha/span = more responsive to recent data

累计操作

cumsum、cummax、cummin、cumprod 是直接累计方法——比 expanding 对常见情况快。配 groupby 计算每组运行总计(如每客户运行余额)。布尔条件上 cumsum 的技巧创建会话/段标识——用于在事件间拆分时间序列成片段。

pandas
s = pd.Series([1, 2, 3, 4, 5])

# Direct cumulative methods (faster than expanding)
s.cumsum()    # [1, 3, 6, 10, 15]
s.cummax()    # [1, 2, 3, 4, 5]
s.cummin()    # [1, 1, 1, 1, 1]
s.cumprod()   # [1, 2, 6, 24, 120]

# Cumulative within groups
df["running_total"] = df.groupby("customer")["amount"].cumsum()
df["rank_in_group"] = df.groupby("customer")["amount"].rank(ascending=False)
df["pct_of_total"] = df["amount"] / df.groupby("customer")["amount"].transform("sum")

# Cumulative count of condition
df["days_since_event"] = (df["event"] == 1).cumsum()
df["event_number"] = df.groupby(
    (df["event"] == 1).cumsum()
).cumcount()

多输出滚动 apply

多滚动统计时,分别计算(window.mean()、window.std())比返回 Series 的单 apply 更清晰且常更快。滚动分位数让你建趋势周围置信带(q10/q90)。建滚动统计 DataFrame 是绘图或喂入 ML 模型前的典型模式。

pandas
# Apply a function returning multiple values per window
def rolling_stats(x):
    return pd.Series({
        "mean": x.mean(),
        "std": x.std(),
        "min": x.min(),
        "max": x.max(),
    })

stats = s.rolling(10).apply(rolling_stats, raw=False)
# This creates a DataFrame with one row per window, columns = the Series index

# Alternative: compute each separately (often clearer)
window = s.rolling(10)
result = pd.DataFrame({
    "mean": window.mean(),
    "std":  window.std(),
    "min":  window.min(),
    "max":  window.max(),
})

# Rolling quantiles
q = pd.DataFrame({
    "q10": s.rolling(20).quantile(0.1),
    "q50": s.rolling(20).quantile(0.5),
    "q90": s.rolling(20).quantile(0.9),
})

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。