Pandas基础:Series 与 DataFrame 入门
pandas 只有两个核心对象:Series 和 DataFrame。前者是带标签的一维数组,后者是一组共享同一套行标签的 Series。日常分析里读数据、筛数据、算统计量,几乎全部操作都落在这两个对象上。
它的设计核心是「标签优先」:运算默认按标签对齐,而不是按位置。这带来了 R 里没有的便利,也带来了 R 里不会遇到的 NaN。
Series:带标签的一维数组
Section titled “Series:带标签的一维数组”import pandas as pd
s = pd.Series( [12.4, 11.8, 13.1, 18.6], index=["S1", "S2", "S3", "S4"], name="expression",)print(s)S1 12.4S2 11.8S3 13.1S4 18.6Name: expression, dtype: float64左边是索引(index),右边是值。索引不是行号,是标签:它可以是字符串、日期、重复值(重复不报错,但会让 loc 返回多行),也可以完全无序。.values 拿到纯 ndarray,.index 拿到索引对象,.dtype 拿到元素类型:
print(s.values)print(s.index)print(s.dtype)[12.4 11.8 13.1 18.6]Index(['S1', 'S2', 'S3', 'S4'], dtype='object')float64索引存在的意义有两个:让「按名字取数」成为可能,以及让多个数据源的运算能自动对齐。第二点在后面单独说。
DataFrame:一组共享索引的 Series
Section titled “DataFrame:一组共享索引的 Series”df = pd.DataFrame({ "sample": ["S1", "S2", "S3", "S4", "S5", "S6"], "group": ["control", "control", "control", "treat", "treat", "treat"], "expression": [12.4, 11.8, 13.1, 18.6, 17.9, 19.3], "purity": [0.94, 0.91, 0.96, 0.88, 0.92, 0.90],})print(df) sample group expression purity0 S1 control 12.4 0.941 S2 control 11.8 0.912 S3 control 13.1 0.963 S4 treat 18.6 0.884 S5 treat 17.9 0.925 S6 treat 19.3 0.90字典的键变成列名,值变成列的内容。构造时没给索引,pandas 自动补了 RangeIndex(0 到 n−1)。取一列有两种写法:df["expression"] 返回 Series,df[["expression"]] 返回只有一列的 DataFrame。混淆这两个是新手最常见的类型错误——你在 Series 上调 df.groupby 或者反过来,报错信息都不太直观。
df["expression"] 和 df.purity 都能取到列,但点号写法有几个限制:列名带空格或和 DataFrame 方法重名(比如列名叫 count)时不能用。工程代码里统一用方括号。
从 sklearn 拿到的数据变成 DataFrame
Section titled “从 sklearn 拿到的数据变成 DataFrame”load_iris() 返回的不是 DataFrame,是 Bunch(可以按属性访问的字典),特征是 ndarray:
from sklearn.datasets import load_iris
iris = load_iris()X = iris.dataprint(type(X))X.head() # 这行会报错<class 'numpy.ndarray'>AttributeError: 'numpy.ndarray' object has no attribute 'head'报错信息里的 'numpy.ndarray' object has no attribute 'head' 是 Python 初学者最常撞见的一条。两种修法,显式构造:
df_iris = pd.DataFrame(iris.data, columns=iris.feature_names)或者让 sklearn 直接给出 DataFrame:
df_iris = load_iris(as_frame=True).frameprint(df_iris.head(3)) sepal length (cm) sepal width (cm) petal length (cm) petal width (cm)0 5.1 3.5 1.4 0.21 4.9 3.0 1.4 0.22 4.7 3.2 1.3 0.2as_frame=True 时 iris.frame 会比 iris.data 多一列 target(数值标签),列名直接来自 iris.feature_names,省掉手写列名的步骤。
三行代码看清一份数据
Section titled “三行代码看清一份数据”拿到任何一张新表,先跑这三个方法:
print(df.head(3))print(df.info())print(df.describe()) sample group expression purity0 S1 control 12.4 0.941 S2 control 11.8 0.912 S3 control 13.1 0.96<class 'pandas.core.frame.DataFrame'>RangeIndex: 6 entries, 0 to 5Data columns (total 4 columns): # Column Non-Null Count Dtype--- ------ -------------- ----- 0 sample 6 non-null object 1 group 6 non-null object 2 expression 6 non-null float64 3 purity 6 non-null float64dtypes: float64(2), object(2)memory usage: 240.0+ bytes expression puritycount 6.000000 6.000000mean 15.516667 0.918333std 3.431278 0.028578min 11.800000 0.88000025% 12.575000 0.90250050% 15.500000 0.91500075% 18.425000 0.935000max 19.300000 0.960000head() 看列名和数据长什么样,info() 看行数、每列的非空数量(Non-Null Count)和 dtype——非空数量少于总行数就说明那列有缺失值,这里是 6 对 6,一点不缺。describe() 只统计数值列,字符串列会被自动跳过;要连字符串列一起看就写 df.describe(include="all")。
describe() 的 25%、50%、75% 是分位数(quartile),50% 就是中位数。它默认用线性插值,所以 6 个点算出来的中位数 15.5 并不出现在原始数据里——这不是错误。
还有一点第一次用容易愣住:describe() 的 std 和 NumPy 的 .std() 不是一回事。pandas 默认除以 n−1(样本标准差),NumPy 默认除以 n(总体标准差)。同一列数据两边算出不同的数,属正常现象,别拿去当 bug 查。
loc 与 iloc
Section titled “loc 与 iloc”这两个索引器覆盖了九成取数需求,区别只有一条:loc 按标签,iloc 按位置。麻烦的是它们的切片规则也不一样——loc 的切片两端都包含,iloc 的切片右端不包含,跟 Python 列表一致。
print(df.loc[0:2, "sample"])print(df.iloc[0:2, 0])0 S11 S22 S3Name: sample, dtype: object0 S11 S2Name: sample, dtype: object默认的 RangeIndex 让标签和位置长得一模一样,loc 和 iloc 写错了也不报错,只是少一行或多一行。把索引换成有业务含义的列之后,区别立刻显现:
grouped = df.set_index("sample")print(grouped.loc[["S2", "S5"], ["expression", "purity"]]) expression puritysampleS2 11.8 0.91S5 17.9 0.92标签索引下 loc[["S2", "S5"]] 里的字符串必须真实存在,写错一个字母就是 KeyError,这个失败是显式的,比默默取错行要好。用 iloc 只认整数位置,iloc[0] 永远取第一行,无论索引被改成了什么。
什么时候用 at / iat:只需要取单个标量时它们更快,df.at["S2", "purity"] 对应 loc,df.iat[1, 3] 对应 iloc。循环里逐个取值时这个区别能看出性能差异,不过真要写循环,通常说明该换成向量化写法了。
这是 pandas 最有价值也最容易吃亏的机制。两个 Series 相加时,pandas 不是按位置逐个加,而是先按索引匹配,只把标签相同的元素相加,匹配不上的位置填 NaN。
control = pd.Series([12.4, 11.8, 13.1], index=["S1", "S2", "S3"])treat = pd.Series([18.6, 17.9, 19.3], index=["S3", "S4", "S5"])
print(control + treat)S1 NaNS2 NaNS3 31.7S4 NaNS5 NaNdtype: float64只有 S3 两边都有。结果索引是两边的并集。R 里对长度不同的向量做运算会触发循环补齐(recycling)并给出警告,Python 这边则是静默产生 NaN——NaN 一旦混进均值、相关系数,后面所有结论都是错的,而且不会报错。
三种处理方式,取决于你真正想要什么:
# 1. 确认两边索引完全一致(顺序也一致)print(control.index.equals(treat.index))
# 2. 按位置对齐:丢掉索引标签print(control.reset_index(drop=True) + treat.reset_index(drop=True))
# 3. 缺失位置当 0 处理print(control.add(treat, fill_value=0))False0 31.01 29.72 32.4dtype: float64S1 12.4S2 11.8S3 31.7S4 17.9S5 19.3dtype: float64第三种写法在合并两批不同来源的计数、金额时用得很多。需要更细的控制可以用 a.align(b, join="inner") 先对齐再运算。
链式赋值与 SettingWithCopyWarning
Section titled “链式赋值与 SettingWithCopyWarning”想给满足条件的行赋值,下面这种写法是错的:
df[df["group"] == "treat"]["purity"] = 0.0df[df["group"] == "treat"] 先产生一个中间对象,赋值赋给了这个临时对象,随后它被丢弃,原始 df 一点没变。pandas 无法预判中间对象是视图还是副本,于是给出 SettingWithCopyWarning;在新版本逐步启用写时复制(Copy-on-Write)之后,这种写法会直接静默失效。正确写法是把条件写进 loc 的第一个参数:
df.loc[df["group"] == "treat", "purity"] = 0.0需要一份独立子集时,显式 .copy():
treat_df = df[df["group"] == "treat"].copy()写时复制开关可以用 pd.options.mode.copy_on_write = True 打开,打开后 pandas 会保证任何一次索引操作的结果都是独立对象,改它不会污染原数据。想要一份可以放心改的中间结果,最省心的做法始终是 .copy()。
R 的数据框在这一点上规则更简单:df[df$group == "treat", "purity"] <- 0 就是原地修改。R 数据框的创建、索引与分组统计可以参考 /r/basics/data-frames/,两边对照着看,能更快建立「哪些操作会复制数据」的直觉。
KeyError: 'name'
列名拼错,或者用点号访问了一个不存在或不合法(含空格)的列名。先 print(df.columns) 看一遍实际列名,常见来源是读 CSV 时表头有空格或 BOM。
数值列变成了 object
多半是那一列里混了非数字字符("<0.05"、"1,024"、"NA" 之类)。用 pd.to_numeric(df["col"], errors="coerce") 强制转换,转不动的会变成 NaN,再用 df["col"].isna() 定位到具体哪些行被转换掉了。
改了切片,原表也跟着变
说明你拿到的是视图。df[["a", "b"]]、df.loc[...] 的返回值可能是视图,需要独立对象就显式 .copy()。
列名有重复怎么办
df["x"] 会返回 DataFrame 而不是 Series,df.groupby 之后的行为也容易出乎意料。用 df.columns.duplicated().sum() 检查,在读取阶段就修掉最省事。
到这里,Python 的基础三件套(环境、ndarray、DataFrame)就齐了。下一步是把数据读进来做筛选和聚合,/python/pandas/data-loading/ 从这里接着往下走。