Pandas 入门:DataFrame、数据清洗与分组统计

从 Series、DataFrame、文件读写到筛选、缺失值处理、分组统计与多表合并,建立 Pandas 表格数据分析的基础框架。

一、Pandas 解决什么问题

如果说 NumPy 更像数值计算的底层能力,那么 Pandas 更像更贴近业务语义的表格处理工具。像 Excel 报表、CSV 统计结果、数据库查询结果、接口返回的列表字典数据,都很适合交给它处理;相比原生 Python 里“列表套字典”的写法,Pandas 在筛选、排序、分组、缺失值处理、统计分析以及文件读写上都更顺手,而且和 NumPyMatplotlib 的衔接也很自然。

二、两个核心对象:SeriesDataFrame

Series

Series 可以理解为“带索引的一维数据”。

import pandas as pd

s = pd.Series([88, 92, 79], index=["Alice", "Bob", "Cindy"])

print(s)
print(s["Bob"])

它很像“有标签的一列数据”。

DataFrame

DataFrame 是 Pandas 最核心的数据结构,可以理解为“带行列标签的二维表格”。

import pandas as pd

df = pd.DataFrame({
    "name": ["Alice", "Bob", "Cindy"],
    "age": [23, 25, 22],
    "score": [88, 92, 79],
})

print(df)
print(df.columns)
print(df.shape)

这里可以先记住两个最常见的标签概念:

  • index:行标签,用来标识每一行
  • columns:列标签,用来标识每一列,也就是表头

前面这个示例里,"name""age""score" 本身就是列标签,所以 df.columns 会得到这些列名。

如果想手动指定列标签,也可以显式传入 columns

import pandas as pd

scores = [
    [88, 91, 95],
    [76, 85, 82],
]

df = pd.DataFrame(
    scores,
    index=["张三", "李四"],
    columns=["语文", "英语", "数学"],
)

这时:

  • index=["张三", "李四"] 表示两行数据分别是谁
  • columns=["语文", "英语", "数学"] 表示每一列分别是哪一门科目

大多数数据分析工作,最终都围绕 DataFrame 展开。

三、创建与读取数据

除了直接写字典,Pandas 更常见的入口是读文件。

import pandas as pd

df = pd.read_csv("sales.csv")
print(df.head())

常见读写函数包括:

  • pd.read_csv()
  • pd.read_excel()
  • df.to_csv()
  • df.to_excel()

初学阶段,至少要先掌握 CSV 的读取与导出。

四、查看数据的常用方法

import pandas as pd

df = pd.read_csv("sales.csv")

# 默认查看前 5 行,快速确认字段名和数据大致长什么样
print(df.head())
# 默认查看后 5 行,确认尾部数据是否完整
print(df.tail())
# 查看每一列的数据类型、非空数量和内存占用
df.info()
# 查看数值列的统计摘要,如均值、最大值、最小值
print(df.describe())
#这是接手一个陌生数据集时最常见的第一步。
# 1.导包
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib

matplotlib.use('TkAgg')
# 2.读取数据
train = pd.read_csv('data/titanic_train.csv')
test = pd.read_csv('data/titanic_test.csv')
# 3.了解数据
print(train.shape)  # (892, 12)
train.info()  # 每列不为空的个数
print(train.count())
print('==================================================')
# 4.缺失值查看
# 4.1 查看是否是缺失值,结果是True或者False
print(train.isna())
print('----------------------------------')
# 4.2 获取每列有缺失值的个数(True:1,Fasle:0)
print(train.isna().sum())
print('----------------------------------')
# 4.3 获取每列缺失值的比例
print(train.isna().sum() / len(train) * 100)
print('==================================================')
# 5.了解生存和死亡的人数,以及可视化展示
# 查看Survived各个值的个数
result = train['Survived'].value_counts()
print(result, type(result))
# 可视化展示
result.plot(kind='bar')  # 柱状图
plt.grid()
# 展示
plt.show()

五、选择列、筛选行

import pandas as pd

df = pd.DataFrame({
    "name": ["Alice", "Bob", "Cindy", "David"],
    "city": ["Beijing", "Shanghai", "Beijing", "Shenzhen"],
    "score": [88, 92, 79, 95],
})

print(df["name"])
print(df[["name", "score"]])

high_score = df[df["score"] >= 90]
print(high_score)

打印结果:

0    Alice
1      Bob
2    Cindy
3    David
Name: name, dtype: object

    name  score
0  Alice     88
1    Bob     92
2  Cindy     79
3  David     95

    name      city  score
1    Bob  Shanghai     92
3  David  Shenzhen     95

最常见的两类操作就是:

  • 选哪些列
  • 保留哪些行

这也是数据清洗和分析的起点。

六、lociloc

Pandas 中两个特别重要的定位方式是:

  • loc:按标签取值
  • iloc:按位置取值
import pandas as pd

df = pd.DataFrame({
    "name": ["Alice", "Bob", "Cindy"],
    "score": [88, 92, 79],
}, index=["a", "b", "c"])

print(df.loc["b", "score"])  
# 按标签取值:取行index赋值的标签为 b、列标签为 score 的单元格,结果是 92
print(df.iloc[1, 1])  
# 按位置取值:取第 2 行、第 2 列的单元格,结果也是 92

两者经常会被混淆,建议在练习时刻意区分“标签”和“位置”。

七、缺失值处理

真实数据几乎一定会遇到缺失值。开始处理之前,先要知道 Pandas 里常见的“空值”长什么样。

import numpy as np
import pandas as pd

print(None, type(None))      # Python 原生空值
print(np.nan, type(np.nan))  # NumPy 中常见的空值
print(pd.NA, type(pd.NA))    # Pandas 里的缺失值标记

判断空值时,通常优先使用 isna() / notna(),而不是直接写 ==。 这是因为不同类型的缺失值在比较行为上并不完全一致,而 isna() / notna() 能统一处理。

import numpy as np
import pandas as pd

print(pd.isna("null"))   # False
print(pd.isna(""))       # False
print(pd.isna(None))     # True
print(pd.isna(np.nan))   # True
print(pd.isna(pd.NA))    # True
print("-" * 36)
print(pd.notna("null"))  # True
print(pd.notna(""))      # True
print(pd.notna(None))    # False
print(pd.notna(np.nan))  # False
print(pd.notna(pd.NA))   # False
#处理表格 (keep_default_na=False,表示不保留默认的缺失值标记)

# 2.读取文件
# 方式1: 默认keep_default_na是True
# 代表自动用NaN填充所有缺失值(空,NULL,null,None,NA,nan)
df = pd.read_csv('data/survey_visited.csv')
print(df)
print('-----------------------------------------------')
# 方式2: 修改keep_default_na为False
# 代表所有缺失值(空,NULL,null,None,NA,nan)位置不填充NaN
df = pd.read_csv('data/survey_visited.csv', keep_default_na=False)
print(df)
print('-----------------------------------------------')
# 方式3: 修改keep_default_na为False,使用na_values指定要用NaN填充的值
# 代表所有缺失值(空,NULL,null,None,NA,nan)位置不填充NaN,na_values=[None,np.nan,pd.NA]
df = pd.read_csv('data/survey_visited.csv', keep_default_na=False, na_values=[None, np.nan, pd.NA])
print(df)

图片 其中:

  • isna()isnull() 基本等价,常用来判断是否为空值
  • notna()notnull() 基本等价,常用来判断是否不是空值
  • 在 Pandas 实战里,更推荐记 isna() / notna() 这一组现代写法

接下来,再看 DataFrame 里怎么处理缺失值:

import pandas as pd

df = pd.DataFrame({
    "name": ["Alice", "Bob", "Cindy"],
    "score": [88, None, 79],
    "city": ["Beijing", "Shanghai", None],
})

print(df.isna())
print(df.dropna())
print(df.fillna({"score": 0, "city": "Unknown"}))

打印结果:

    name  score   city
0  False  False  False
1  False   True  False
2  False  False   True

    name  score     city
0  Alice   88.0  Beijing

    name  score      city
0  Alice   88.0   Beijing
1    Bob    0.0  Shanghai
2  Cindy   79.0   Unknown

常见处理方式有:

  • 先用 isna() 找出缺失位置
  • 直接丢弃缺失行 dropna()
  • 用固定值填充 fillna()
  • 用均值、中位数、前值后值等策略填充

到底选哪一种,取决于业务场景,而不是单纯的语法问题。

fillna() 的常见填充策略

当数据不能直接删除时,更常见的思路是先考虑怎么填充缺失值。

import warnings
import pandas as pd

# 演示时可以临时忽略警告,正式项目里不要默认全局关闭
warnings.filterwarnings("ignore")

df = pd.DataFrame({
    "name": ["Tom", "Lucy", "Anna", "Mike"],
    "age": [18, None, 20, None],
    "city": ["Beijing", None, "Shanghai", "Shenzhen"],
})

# 1. 直接填充 0
new_df1 = df.fillna(0)
print(new_df1)

# 2. 用 age 列平均值填充
new_df2 = df.fillna({"age": df["age"].mean()})
print(new_df2)

# 3. 用前一个非缺失值向后填充
new_df3 = df.ffill()
print(new_df3)

# 4. 用后一个非缺失值向前填充
new_df4 = df.bfill()
print(new_df4)

这几种方式各有适用场景:

  • fillna(0) 简单直接,但不一定合理
  • fillna({"age": df["age"].mean()}) 适合数值列做统计型填充
  • ffill() 适合时间顺序数据或记录延续场景
  • bfill() 适合“后一个值更可信”的补齐场景

为什么不建议看到空值就直接填 0

  • 如果 age 这类字段本来不可能是 0,强行填 0 会拉低平均值
  • 如果后面还要做聚合、建模或可视化,结果可能被这种“伪数据”带偏

另外补充一点:

  • 你在很多旧教程里会看到 df.fillna(method="ffill")df.fillna(method="bfill")
  • 现在更推荐直接写 df.ffill()df.bfill(),语义更直观,也更符合新版 Pandas 的写法

所以,填充策略通常可以这样选:

  1. 能删且影响不大,就优先删
  2. 不能轻易删时,再考虑 fillna()
  3. 数值型字段优先考虑均值、中位数等统计值
  4. 有明显顺序关系的数据,再考虑前向或后向填充

dropna()drop() 的常见删除方式

很多初学者会把“删除缺失值”和“删除某一列”混在一起。可以先这样区分:

  • dropna():根据是否为空值来删行或删列
  • drop():按标签直接删除指定行或指定列

下面这个例子和真实表格分析更接近:

import pandas as pd

train = pd.DataFrame({
    "Name": ["Alice", "Bob", "Cindy", "David"],
    "Age": [22, None, 19, None],
    "Cabin": [None, None, "C85", None],
    "Fare": [7.25, 71.83, 8.05, 13.0],
})

# 1. 默认 how="any":只要这一行里有任意缺失值,就删除该行
new_train1 = train.dropna()
print(new_train1)

# 2. how="all":只有整行全是缺失值时才删除
new_train2 = train.dropna(how="all")
print(new_train2)

# 3. subset=["Age"]:只检查 Age 这一列,只要 Age 缺失就删除该行
new_train3 = train.dropna(subset=["Age"])
print(new_train3)

# 4. inplace=True:直接在原对象上修改
temp = train.copy()
temp.dropna(subset=["Age"], inplace=True)
print(temp)

# 5. drop(..., axis=1):按列名直接删除某一列
new_train4 = train.drop("Cabin", axis=1)
print(new_train4)

这里最常见的几个点是:

  • dropna() 默认等价于 dropna(how="any")
  • how="all" 适合删除“整行都没值”的脏数据
  • subset=["Age"] 适合“只关心关键列是否为空”的场景
  • inplace=True 表示直接修改原对象;不写时,通常会返回一个新结果
  • drop("Cabin", axis=1) 表示按列删除,axis=1 代表列方向

在像 Titanic 这种数据里,Cabin 往往缺失很多,这时直接删除这一列,有时比删除大量样本行更合理。

所以缺失值处理时,常见决策顺序往往是:

  1. 先判断缺失发生在哪些列、比例有多高
  2. 关键列少量缺失时,用 dropna(subset=...) 删除对应行
  3. 非关键列缺失过多时,考虑 drop(..., axis=1) 删除整列
  4. 如果数据不能轻易删,再考虑 fillna() 做填充

删除没有绝对标准,重点不是“会不会语法”,而是“删掉后会不会影响分析结论”。

八、排序、去重与新增列

import pandas as pd

df = pd.DataFrame({
    "name": ["Alice", "Bob", "Alice"],
    "score": [88, 92, 88],
})

df["passed"] = df["score"] >= 60

print(df.sort_values("score", ascending=False))
print(df.drop_duplicates())

打印结果:

    name  score  passed
1    Bob     92    True
0  Alice     88    True
2  Alice     88    True

    name  score  passed
0  Alice     88    True
1    Bob     92    True

这些操作在数据清洗里非常高频:

  • sort_values() 排序
  • drop_duplicates() 去重
  • df["new_col"] = ... 新增派生列

在 Pandas 中,我们经常将这些操作连在一起写,这种写法叫做链式操作(Chaining)

# 链式操作示例:先按 score 排序,然后去除 name 重复的行
result = df.sort_values("score", ascending=False).drop_duplicates(subset=["name"])
print(result)

九、分组统计:groupby

groupby 是 Pandas 最核心的分析能力之一。

import pandas as pd

df = pd.DataFrame({
    "city": ["Beijing", "Beijing", "Shanghai", "Shanghai"],
    "sales": [120, 150, 200, 180],
})
# 按 city 分组后,对每个城市的 sales 求平均值
result = df.groupby("city")["sales"].mean()  
print(result)
"""
打印:
city
Beijing     135
Shanghai    190
Name: sales, dtype: int64
"""

它的直觉可以理解为:

  1. 先按某个字段分组
  2. 再对每组做聚合

常见聚合方法包括: sum() mean() count() max() min()

如果你开始接触真实数据集,很快就会遇到“按多个条件分组”“同时统计多个字段”的需求。

import numpy as np
import pandas as pd

# txt 只是一种文件后缀,只要数据是固定分隔格式,read_csv() 也一样能读
df = pd.read_csv("data/gapminder.txt", sep="\t")
print(df.shape)

# 1. 按 year 分组,求每个年份的平均寿命
print(df.groupby("year").lifeExp.mean())
print(df.groupby("year")["lifeExp"].mean())
print(df.groupby("year")[["lifeExp"]].mean())

# 2. 用 agg() 显式指定字段和聚合函数
print(df.groupby("year").agg({"lifeExp": "mean"}))
print(df.groupby("year").agg({"lifeExp": np.mean}))

# 3. 同时按 year 和 continent 分组,对多个字段做不同聚合
print(
    df.groupby(["year", "continent"]).agg({
        "lifeExp": "mean",
        "gdpPercap": "max",
    })
)
print(
    df.groupby(["year", "continent"]).agg({
        "lifeExp": np.mean,
        "gdpPercap": np.max,
    })
)

这里有几个特别容易混淆的点:

  • df.groupby("year").lifeExp.mean()df.groupby("year")["lifeExp"].mean() 都是在取单列后做聚合
  • df.groupby("year")[["lifeExp"]].mean() 结果仍然是 DataFrame
  • df.groupby("year")["lifeExp"].mean() 结果通常是 Series

也就是说,单中括号和双中括号的区别,在 groupby 后面依然成立:

  • ["lifeExp"] 更像“取一列”,结果偏向 Series
  • [["lifeExp"]] 更像“取一个只含一列的表”,结果仍然是 DataFrame

agg() 的价值在于,它更适合写成“报表式”的聚合逻辑:

  • {"lifeExp": "mean"} 表示对 lifeExp 求平均值
  • {"lifeExp": np.mean} 表示把 NumPy 函数作为聚合函数传进去
  • {"lifeExp": "mean", "gdpPercap": "max"} 表示不同字段可以用不同聚合方式

多字段分组时,可以把它理解成“先按 year 分,再在每个年份里继续按 continent 分”,最后得到的是一个分层索引结果。

这类写法在实际分析里非常常见,例如:

  • 按 年份 统计平均寿命
  • 按 年份 + 大洲 统计平均寿命和最高人均 GDP
  • 按 月份 + 城市 统计订单数、销售额、客单价

如果你有一点 SQL 基础,可以把它和下面这种思路对应起来:

SELECT year, continent, AVG(lifeExp), MAX(gdpPercap)
FROM gapminder
GROUP BY year, continent;

十、透视表:pivot_table

很多报表类需求,本质上都是“按行分组、按列展开、交叉汇总”。这正是透视表擅长的场景。

import pandas as pd

df = pd.DataFrame({
    "month": ["Jan", "Jan", "Feb", "Feb", "Feb"],
    "city": ["Beijing", "Shanghai", "Beijing", "Shanghai", "Beijing"],
    "sales": [120, 150, 130, 160, 140],
})

table = pd.pivot_table(
    df,
    index="month",
    columns="city",
    values="sales",
    aggfunc="sum",
    fill_value=0,
)

print(table)

打印结果:

city   Beijing  Shanghai
month
Feb        270       160
Jan        120       150

这几个参数最值得先记住:

  • index:透视表的行分组依据
  • columns:透视表的列分组依据
  • values:要统计的目标值
  • aggfunc:聚合方式,比如 summeancount
  • fill_value:把缺失位置补成指定值,避免出现 NaN

可以把它理解成“先 groupby,再把结果重新排成更适合看报表的二维结构”。

如果你只是做简单重排,也会看到 pivot()。但初学阶段更推荐优先掌握 pivot_table(),因为它可以直接处理重复组合并做聚合,而 pivot() 遇到重复键时通常会报错。

十一、多表合并:mergeconcat

真实分析场景里,数据往往不会只放在一张表里。

例如:

  • 订单表里有 product_id
  • 商品表里有 product_id 和商品名称
  • 不同月份的数据可能分散在多张结构相同的表里

这时就会经常用到:

  • merge():按共同字段把两张表“横向关联”起来
  • concat():把多张结构相同的表“纵向拼接”起来
import pandas as pd

orders = pd.DataFrame({
    "product_id": [101, 102, 101],
    "sales": [1200, 800, 1500],
})

products = pd.DataFrame({
    "product_id": [101, 102],
    "product_name": ["Keyboard", "Mouse"],
})

jan = pd.DataFrame({
    "month": ["Jan", "Jan"],
    "sales": [1200, 800],
})

feb = pd.DataFrame({
    "month": ["Feb", "Feb"],
    "sales": [1500, 950],
})

merged = orders.merge(products, on="product_id")
combined = pd.concat([jan, feb], ignore_index=True)

print(merged)
print(combined)

打印结果:

   product_id  sales product_name
0         101   1200     Keyboard
1         101   1500     Keyboard
2         102    800        

  month  sales
0   Jan   1200
1   Jan    800
2   Feb   1500
3   Feb    950

可以先这样理解:

  • merge(..., on="字段名") 类似数据库里的关联查询
  • concat([...]) 更像把多份同结构数据接在一起

初学阶段先掌握这两种最常见的合并思路,就已经能覆盖很多报表场景。

十二、一个简单示例:分析销售表

import pandas as pd

df = pd.DataFrame({
    "product": ["A", "A", "B", "B"],
    "month": ["Jan", "Feb", "Jan", "Feb"],
    "sales": [1200, 1500, 800, 950],
})

monthly_total = df.groupby("month")["sales"].sum()
product_mean = df.groupby("product")["sales"].mean()

print("按月总销量:")
print(monthly_total)
print("按产品平均销量:")
print(product_mean)

像这种“分组后做汇总”的需求,在报表分析中非常常见。

十三、Pandas 与 NumPy、Matplotlib 的关系

把三者看成一条数据分析链路上的三个环节:NumPy 提供底层数组与数值计算能力,Pandas 负责表格数据处理,Matplotlib 负责把结果画出来。在很多日常分析里,你最常直接写的是 Pandas + Matplotlib,而 NumPy 往往作为底层基础存在。

三点最小对比:

  • 核心对象上,NumPy 主要是 ndarrayPandas 主要是 SeriesDataFrameMatplotlib 常见的是 figureaxes
  • 数据形态上,NumPy 更适合处理同质数值数组,Pandas 更适合处理带行列标签的表格数据,Matplotlib 本身不负责组织数据,而是接收前两者处理后的结果来绘图
  • 常见操作上,NumPy 侧重数学运算、数组运算和广播,Pandas 侧重读取、筛选、转换、合并、分组统计,Matplotlib 侧重图表类型选择、坐标轴设置、图例和样式调整

学习顺序可以简单记成:先学 NumPy,再学 Pandas,最后学 Matplotlib

小结

Pandas 是 Python 数据分析里最常用、也最贴近业务问题的工具。只要你的数据长得像“表”,十有八九都能先想到 DataFrame

先掌握读取数据、筛选清洗、分组统计、透视表和多表合并这几类核心操作,再继续深入时间序列,会更稳一些。