入门
安装与导入
pandas 按惯例导入为 pd,numpy 为 np,因为 pandas 底层使用 numpy 数组。两个核心数据结构是 Series(一维带标签数组)和 DataFrame(二维带标签表)。Python 中几乎所有表格数据操作都从这里开始。
# 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。
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)使后续选择和连接更干净。
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 对快速复制粘贴分析出奇地好用。
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' 覆盖。
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)数据检视
查看数据:head、tail、sample
head/tail 显示边缘;sample 显示随机行,对分布和格式做合理性检查更具代表性。用 set_option 禁用宽 DataFrame 检视时的截断——默认会隐藏列和行。任何分析前都看一眼样本数据,以尽早发现编码问题、哨兵值和类型不匹配。
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 列的真实开销,常比报告的大得多。
# 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 避免错误。
# 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 揭示分布形状。
# 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 库可视化缺失模式以发现结构性数据问题。
# 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))Series 基础
创建 Series
Series 是带标签(索引)的一维数组。索引让你按标签查值,不只是按位置。指定更小 dtype(int8、float32)在大数据上省内存——int8 持 -128..127,对布尔转整数或小编码足够。字典键自然成为索引。
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 更安全。
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 倍。
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)。
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 支持线性、时间和索引方法。
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", ""])选择与筛选
选择列
括号表示 df['col'] 总是有效;点表示 df.col 方便但对含空格或与方法同名(如 'count')的名称失效。filter(like=) 和 select_dtypes 是无需逐个列名抓取列组的干净方式——对含数百列的宽 DataFrame 极有用。
# 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 支持布尔掩码(基于标签筛选),这是筛选并赋值的干净方式。
# .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;赋值给变量以保留。
# 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 链和管道内尤其好用。
# 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 代码可靠。加权采样对重要性采样或模拟有偏数据有用。
# 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)数据清洗
处理缺失数据
dropna(how='all') 只丢全空行;thresh 保留至少 N 个非空值的行。用字典按列填充比多次 fillna 调用干净。时间序列用 ffill/bfill 和插值保持连续性。始终考虑缺失本身是否有信息量——填充前加 'is_missing' 指示列可为 ML 保留该信号。
# 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)去重。
# 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' 头疼。
# 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 倍内存。日期务必用显式格式字符串解析以快速正确。
# 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 是其反。这些都返回新对象——重新赋值以应用。
# 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)数据变换
apply 与 applymap
apply 按元素/行运行 Python 函数——灵活但慢(比向量化慢 100 倍)。可能时始终优先向量化运算(df['col'] + 1、.str.upper())。apply 用于无法向量化的逻辑(自定义字符串解析、引用多列的条件变换)。逐行用 axis=1 并通过 row['col'] 访问列。
# 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'] 清晰得多。
# 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 聚合。
# 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= 控制该行为。
# 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。
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")]分组与聚合
基础 GroupBy
groupby 遵循拆分-应用-合并:按组键拆分行,每组应用函数,合并结果。groupby('city')['age'].mean() 返回按城市索引的 Series;带列表的 agg 返回 DataFrame。size() 计每组所有行(含 NaN 键);value_counts() 排除 NaN 并按频率排序。多组键创建层次索引。
# 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()。
# 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 按每组布尔条件保留或丢弃整组(如丢样本太少的城市)。
# 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 返回最大值的索引——用它查同行其他列。
# 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 是其逆,宽转长格式。
# 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")合并与连接
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')在连接基数错误时报错——防意外行重复的好护栏。
# 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 expectedconcat(堆叠)
concat 垂直(axis=0,默认)或水平(axis=1)堆叠 DataFrame。ignore_index=True 丢弃原索引创建干净 0..n-1——堆叠时通常是你想要的。join='inner' 只保留共享列(帧漂移时有用)。verify_integrity=True 对重复索引报错,是乱追加的安全网。
# 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 missingjoin(基于索引)
join 是 merge 对连接索引常见情况的语法糖——两帧都已有有意义索引时更简洁。按列连接时 merge 更清晰。join 可一次连接多个 DataFrame(传列表),把多个查找表合并到主帧很方便。
# 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。
# 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。
# 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重塑数据
melt(宽转长)
melt 把宽格式数据(每实体一行,多测量列)转长格式(每测量一行),这是大多数 ML/统计库偏爱的整洁格式。id_vars 是保持原样的标识列;其余变 var/value 对。长格式更易 groupby、筛选和绘图。
# 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 的逆。
# 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 = Nonestack 与 unstack
stack/unstack 在 MultiIndex 对象的行和列之间移动层级——对重塑分组结果强大。多键 groupby 产生带 MultiIndex 的 Series;unstack 把一层转列得矩阵视图。stack 是其逆。这些是透视表底层的构建块。
# 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)。
# 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 平回普通列,更易处理。
# 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()排序与排名
排序值
带列列表和匹配 ascending 列表的 sort_values 按字典序排序——主键先,平局用次键。na_position 控制 NaN 去哪(默认 'last')。排序稳定,等键保持原序。排序修改索引顺序;之后 reset_index(drop=True) 给干净连续索引。
# 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 计算组内排名(如每区域销冠)。
# 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')。
# 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[[...]] 是重排和子集列的最清晰方式。
# 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)需拆分数字的自定义键。
# 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))日期时间操作
解析日期
知道格式时始终传显式格式字符串——比推断快 10 倍且避免歧义(01/02/2024 可能是 1 月 2 日或 2 月 1 日)。errors='coerce' 把不可解析值转 NaT(时间 NaN,Not a Time)。对含年/月/日列的 DataFrame 用 pd.to_datetime 从部分构造日期。
# 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 天。
# 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' 参数控制箱边界属于左还是右周期——对周/月聚合避免差一错误重要。
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。
# 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')正确处理不规则采样,不像行计数窗口。
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分类数据
创建分类
category dtype 把字符串存为整数码加查找表——对低基数列省大量内存(百万行 5 个唯一值的城市列)。有序分类支持比较(size > 'M')且按类别顺序排序,非字母。对已知小值集列用 category。
# 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;重新赋值以应用。
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。
# 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 值得。
# 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。
# 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"]])性能与优化
向量化(避免 iterrows)
iterrows 是 pandas 头号性能罪——比向量化运算慢 100-1000 倍。始终优先向量化列运算(df['a'] + df['b'])、.str 字符串、np.where if-else。确需循环时 itertuples 比 iterrows 快得多(具名元组 vs Series)。复杂逻辑考虑 numba 或 cython,或用 .to_numpy() 提取 numpy。
# 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 级求值。小数据收益缩小(解析开销)。对大帧复杂表达式有用——但始终基准测试,因不总更快。
# 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 对几乎全零数据有帮助。
# 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。
# 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' 把它们作为错误捕获。
# 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数据库与 SQL
从 SQL 读取
SQLAlchemy 引擎处理连接池和方言差异——用它们而非原始 DBAPI 连接。用户提供的值始终用参数化查询(params=)防 SQL 注入。chunksize 流式处理大表分批。复杂查询用 SQL 写(数据库为之优化),让 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。
# 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' columnpandas 中的 SQL 风格操作
大多数 SQL 操作映射到 pandas:SELECT 是列选择,WHERE 是布尔索引,GROUP BY 是 groupby,JOIN 是 merge,UNION 是 concat+drop_duplicates,CASE WHEN 是 np.where。知道这些映射让你选对工具:复杂连接/聚合用 SQL(让 DB 优化),迭代分析和绘图用 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 字典绑定参数防注入。
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。
# 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时间序列分析
设置 DatetimeIndex
pandas 时间序列工作从 DatetimeIndex 开始——把日期列设为索引并排序。然后可用字符串切片(df.loc['2024-06'] 取整个六月)及 resample/rolling/shift。部分字符串索引极方便。设置索引后始终排序;许多时间序列方法要求有序索引。
# 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() 聚合取每期最后值。
# 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' 处理。
# 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 清理。
# 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 显示序列与自身滞后的相关性。
# 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)可视化
基础图表
pandas 绘图建在 matplotlib 上——.plot 访问器为 EDA 给快速图表。plot() 默认线图(时间序列好),plot.bar() 类别计数,plot.hist() 分布,plot.scatter() 关系。出版级图表转 seaborn 或 plotly,但分析期间快速检视 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 为整洁数据设计。
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 优秀。
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 对子图尺寸提供更细控制。
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 按行/列。
# 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")导出与格式化
格式化显示
显示选项控制 pandas 在控制台/Jupyter 打印方式——增 max_rows/max_columns 看更多,set float_format 一致小数。option_context 是临时改设置(之后恢复)的上下文管理器,适合一次性检视不全局改环境。常用设置放启动脚本。
# 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' 支持格式公式、条件格式和图表。
# 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 给日志的纯文本表示。
# 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 文件写入时不自动)。冻结窗格滚动时保持表头可见。
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 和缺失,标准化对任何新数据集的第一眼。只抑制特定警告类别,绝不全部。
# 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)常见陷阱
SettingWithCopyWarning
SettingWithCopyWarning 是 pandas 告诉你赋值可能不持久——链式索引(df[mask]['col']=x)可能修改临时副本。修复始终是 .loc[row_mask, col_name] = value 做条件赋值。子集后修改时用 .copy() 显式。开发中把警告设 'raise' 把这些 bug 作错误捕获。
# 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 错误之一。
# 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。
# 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 作哨兵并在函数内初始化。
# 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 的整数。
# 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")实战模式
EDA 模板
标准化 EDA 函数确保建模前始终检查形状、dtype、缺失、分布和重复。尽早捕获错误 dtype(数字作字符串)、隐藏缺失(哨兵值如 -999)或意外重复省下游调试数小时。每个新数据集第一步都跑这个。
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 避免引号头疼。末尾断言捕获静默失败(如所有日期解析失败)。把管道包进函数以在新数据批次复用。
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)在业务报告中极常见。
# 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 为线性模型独热编码。包进函数以使训练和推理时相同变换。
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 前几行。
# 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="\\")窗口操作深入
带自定义函数的滚动
内置滚动聚合(mean、std、median、quantile)是 C 优化且快。自定义 apply 灵活但慢(每窗口 Python 回调)。加权平均或回归时用 apply 内 numpy 运算。raw=True(默认)传 numpy 数组(快);需索引时 raw=False 传 Series。
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' 排除当前点(从『未来』算特征时避免泄露)。
# 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 比简单移动平均对近期变化反应更快。
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 的技巧创建会话/段标识——用于在事件间拆分时间序列成片段。
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 模型前的典型模式。
# 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),
})相关 Pandas 代码片段
Copy-paste ready code for common tasks.
DataFrame Creation
Build DataFrames from dicts, lists, and files.
Indexing and Selecting
Select rows and columns with loc, iloc, and masks.
GroupBy Operations
Split, aggregate, and transform with groupby.
Merge and Join
Combine frames with merge and concat.
Pivot Tables
Reshape data with pivot, melt, and pivot_table.
Time Series
Resample, shift, and roll windows over time data.
Missing Data
Detect, fill, and drop NaN values.
I/O Operations
Read and write CSV, Excel, Parquet, and SQL.
这篇内容对您有帮助吗?