{T}

数据分析概览

在 Python 数据分析领域,NumPy、Pandas 和 Matplotlib 构成了数据处理、分析和可视化的基础设施。本文不重复每个库的 API 手册,而是聚焦于工作流程、工具选择和实战决策

阅读提示

数据分析全景

图表渲染中…

数据分析工作流程

一个典型的数据分析项目遵循结构化工作流程,确保分析过程的系统性和结果的可靠性:

图表渲染中…

各阶段详解

阶段核心任务常用工具耗时占比
定义问题明确分析目标、业务问题、假设头脑风暴、业务访谈5%
数据采集收集内部/外部数据SQL、requests、爬虫10%
数据清洗处理缺失值、异常值、重复值、格式统一Pandas、NumPy60%
探索分析描述性统计、可视化、发现模式Pandas、Matplotlib、Seaborn10%
特征工程创建新特征、转换现有特征Pandas、NumPy10%
建模分析统计检验、机器学习scikit-learn、statsmodels5%
结果呈现图表、报告、仪表盘Matplotlib、Plotly5%
现实中的耗时分布

数据清洗通常占项目 60% 以上的时间,但很多人低估了这一点。如果你发现"分析"阶段很短,大概率是因为清洗做得不够彻底。

核心工具链选择

NumPy vs Pandas vs Polars

对比维度NumPyPandasPolars
数据结构n 维数组 ndarray二维表格 DataFrame二维表格 DataFrame
典型场景数值计算、线性代数表格数据清洗与分析大数据集高性能处理
性能高(C 实现)中(单线程)高(Rust 实现,多线程)
内存效率高(固定类型)中(对象开销)高(Apache Arrow)
学习曲线中(API 不同于 Pandas)
生态成熟度最成熟最成熟快速发展中
适合数据量任意< 5GB 内存> 5GB 或需要速度

选择原则

  • 小数据集 + 快速探索 → Pandas(生态最全,教程最多)
  • 纯数值计算(矩阵运算、信号处理) → NumPy
  • 大数据集 + 性能敏感 → Polars(比 Pandas 快 5-50 倍)

可视化工具选择

工具特点适用场景
Matplotlib底层、灵活、啰嗦完全自定义的出版级图表
Seaborn统计图表、简洁数据探索、统计可视化
Plotly交互式、Web 友好仪表盘、交互报告
Altair声明式语法简洁统计图表
图表渲染中…

NumPy 核心速查

python
import numpy as np

# 创建数组
arr = np.array([1, 2, 3, 4, 5])
matrix = np.array([[1, 2], [3, 4]])

# 常用操作
arr.mean()        # 均值: 3.0
arr.std()         # 标准差: 1.414...
arr.max()         # 最大值: 5
arr.min()         # 最小值: 1
arr.sum()         # 求和: 15

# 向量化运算(避免 Python 循环)
prices = np.array([100, 200, 300])
rates = np.array([0.1, 0.2, 0.15])
discounted = prices * (1 - rates)  # [90. 160. 255.]

# 布尔索引筛选
scores = np.array([85, 92, 78, 95, 60, 88])
high_scores = scores[scores >= 80]  # [85 92 95 88]

# 常用创建函数
np.zeros((3, 4))       # 3×4 零矩阵
np.ones((2, 3))        # 2×3 全1矩阵
np.arange(0, 10, 2)    # [0 2 4 6 8]
np.linspace(0, 1, 5)   # [0. 0.25 0.5 0.75 1.]
np.random.randn(3, 3)  # 3×3 标准正态分布
向量化 vs 循环

NumPy 的核心价值是向量化运算:用数组运算替代 Python 循环。性能差距可达 100 倍以上。

python
# ❌ 慢:Python 循环
result = []
for x in data:
    result.append(x ** 2 + 2 * x + 1)

# ✅ 快:NumPy 向量化
result = data ** 2 + 2 * data + 1

Pandas 核心速查

python
import pandas as pd

# 读取数据
df = pd.read_csv("sales.csv")
df = pd.read_excel("data.xlsx", sheet_name="Sheet1")
df = pd.read_json("api_response.json")

# 查看数据
df.head()                    # 前5行
df.info()                    # 列信息 + 类型 + 缺失值
df.describe()                 # 数值列统计摘要
df.shape                      # (行数, 列数)
df.columns.tolist()           # 列名列表
df.dtypes                     # 每列数据类型

# 筛选与过滤
df[df["age"] > 30]                          # 条件筛选
df[["name", "age", "salary"]]               # 选择列
df.loc[df["city"] == "Beijing", "salary"]   # label 筛选

# 排序
df.sort_values("salary", ascending=False)    # 降序
df.sort_values(["city", "age"])             # 多列排序

# 分组聚合
df.groupby("city")["salary"].mean()         # 按城市分组求平均薪资
df.groupby("city").agg(
    avg_salary=("salary", "mean"),
    max_salary=("salary", "max"),
    count=("salary", "count"),
)

# 缺失值处理
df.isnull().sum()                           # 每列缺失值数量
df.dropna(subset=["email"])                 # 删除 email 列缺失的行
df["age"].fillna(df["age"].median())        # 用中位数填充

# 新增列
df["salary_k"] = df["salary"] / 1000
df["is_senior"] = df["years"] >= 5

# 时间处理
df["date"] = pd.to_datetime(df["date"])
df["month"] = df["date"].dt.month
df.set_index("date").resample("M")["sales"].sum()  # 按月汇总

# 导出
df.to_csv("output.csv", index=False)
df.to_parquet("output.parquet")             # 更快更小

Matplotlib 核心速查

python
import matplotlib.pyplot as plt

# 基础折线图
plt.figure(figsize=(10, 6))
plt.plot(months, revenue, marker='o', linewidth=2, label='营收')
plt.plot(months, cost, marker='s', linestyle='--', label='成本')
plt.xlabel('月份')
plt.ylabel('金额(万元)')
plt.title('月度营收与成本趋势')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('trend.png', dpi=150)
plt.show()

# 子图布局
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes[0, 0].bar(categories, values)
axes[0, 1].hist(data, bins=30)
axes[1, 0].scatter(x, y, alpha=0.6)
axes[1, 1].pie(sizes, labels=labels, autopct='%1.1f%%')
fig.suptitle('数据分析概览', fontsize=16)
plt.tight_layout()

实战场景

场景一:CSV 数据清洗管道

python
import pandas as pd
import numpy as np


def clean_sales_data(input_path: str, output_path: str) -> pd.DataFrame:
    """完整的 CSV 数据清洗管道"""

    # 1. 读取数据
    df = pd.read_csv(input_path)
    print(f"原始数据: {df.shape[0]} 行, {df.shape[1]} 列")

    # 2. 列名标准化
    df.columns = df.columns.str.strip().str.lower().str.replace(' ', '_')

    # 3. 去除完全重复的行
    before = len(df)
    df = df.drop_duplicates()
    print(f"去重: {before - len(df)} 行")

    # 4. 处理缺失值
    # 数值列用中位数填充
    numeric_cols = df.select_dtypes(include=[np.number]).columns
    for col in numeric_cols:
        missing = df[col].isnull().sum()
        if missing > 0:
            df[col] = df[col].fillna(df[col].median())
            print(f"  {col}: 填充 {missing} 个缺失值(中位数)")

    # 分类列用众数填充
    categorical_cols = df.select_dtypes(include=['object']).columns
    for col in categorical_cols:
        missing = df[col].isnull().sum()
        if missing > 0:
            mode_value = df[col].mode()[0]
            df[col] = df[col].fillna(mode_value)
            print(f"  {col}: 填充 {missing} 个缺失值(众数: {mode_value})")

    # 5. 处理异常值(IQR 法)
    for col in numeric_cols:
        q1 = df[col].quantile(0.25)
        q3 = df[col].quantile(0.75)
        iqr = q3 - q1
        lower = q1 - 1.5 * iqr
        upper = q3 + 1.5 * iqr
        outliers = (df[col] < lower) | (df[col] > upper)
        if outliers.sum() > 0:
            df.loc[outliers, col] = df[col].clip(lower, upper)
            print(f"  {col}: 截断 {outliers.sum()} 个异常值")

    # 6. 日期解析
    date_cols = [c for c in df.columns if 'date' in c]
    for col in date_cols:
        df[col] = pd.to_datetime(df[col], errors='coerce')

    # 7. 导出
    df.to_parquet(output_path, index=False)
    print(f"\n清洗完成: {len(df)} 行 → {output_path}")
    return df


# 使用
clean_sales_data("raw_sales.csv", "clean_sales.parquet")

场景二:快速探索性分析报告

python
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


def quick_eda(df: pd.DataFrame, target: str | None = None) -> None:
    """生成快速 EDA 报告"""

    print("=" * 60)
    print("探索性数据分析报告")
    print("=" * 60)

    # 基本信息
    print(f"\n数据维度: {df.shape[0]} 行 × {df.shape[1]} 列")
    print(f"内存占用: {df.memory_usage(deep=True).sum() / 1024 / 1024:.1f} MB")

    # 缺失值
    missing = df.isnull().sum()
    missing_pct = (missing / len(df) * 100).round(2)
    missing_df = pd.DataFrame({"缺失数": missing, "缺失率%": missing_pct})
    missing_df = missing_df[missing_df["缺失数"] > 0].sort_values("缺失率%", ascending=False)
    if len(missing_df) > 0:
        print(f"\n缺失值统计:\n{missing_df.to_string()}")
    else:
        print("\n✅ 无缺失值")

    # 数值列统计
    numeric_df = df.describe().T
    print(f"\n数值列统计:\n{numeric_df.to_string()}")

    # 如果有目标变量
    if target and target in df.columns:
        print(f"\n目标变量 '{target}' 分布:")
        if df[target].dtype == 'object':
            print(df[target].value_counts().to_string())
        else:
            print(f"  均值: {df[target].mean():.2f}")
            print(f"  中位数: {df[target].median():.2f}")
            print(f"  标准差: {df[target].std():.2f}")

    # 生成关键图表
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))

    # 缺失值柱状图
    if len(missing_df) > 0:
        missing_df["缺失率%"].plot.bar(ax=axes[0], color='coral')
        axes[0].set_title('缺失值比例')
        axes[0].set_ylabel('缺失率 (%)')

    # 数值列分布
    numeric_cols = df.select_dtypes(include=[np.number]).columns[:2]
    if len(numeric_cols) >= 1:
        df[numeric_cols[0]].hist(ax=axes[1], bins=30, color='steelblue', alpha=0.7)
        axes[1].set_title(f'{numeric_cols[0]} 分布')

    # 相关性热力图(取 top 8 数值列)
    corr_cols = df.select_dtypes(include=[np.number]).columns[:8]
    if len(corr_cols) > 1:
        corr = df[corr_cols].corr()
        im = axes[2].imshow(corr, cmap='RdBu_r', vmin=-1, vmax=1)
        axes[2].set_xticks(range(len(corr_cols)))
        axes[2].set_yticks(range(len(corr_cols)))
        axes[2].set_xticklabels(corr_cols, rotation=45, ha='right', fontsize=8)
        axes[2].set_yticklabels(corr_cols, fontsize=8)
        axes[2].set_title('相关性矩阵')

    plt.tight_layout()
    plt.savefig('eda_report.png', dpi=150, bbox_inches='tight')
    print("\n📊 图表已保存: eda_report.png")
    plt.show()


# 使用
df = pd.read_csv("sales.csv")
quick_eda(df, target="revenue")

场景三:数据聚合与分组报告

python
import pandas as pd


def generate_sales_report(df: pd.DataFrame) -> pd.DataFrame:
    """生成多维度销售汇总报告"""

    # 按城市和产品分组
    report = df.groupby(["city", "product"]).agg(
        订单数=("order_id", "count"),
        总营收=("amount", "sum"),
        平均客单价=("amount", "mean"),
        最大订单=("amount", "max"),
        最早订单=("order_date", "min"),
        最近订单=("order_date", "max"),
    ).reset_index()

    # 计算环比增长
    report["营收排名"] = report.groupby("city")["总营收"].rank(ascending=False).astype(int)

    # 筛选 Top 3 产品
    top_products = (
        report[report["营收排名"] <= 3]
        .sort_values(["city", "营收排名"])
    )

    return top_products


# 使用
df = pd.read_csv("sales.csv", parse_dates=["order_date"])
report = generate_sales_report(df)
print(report.to_string(index=False))

常见陷阱

陷阱现象原因解决方案
SettingWithCopyWarningPandas 发出链式赋值警告修改了 DataFrame 的视图而非副本.copy() 显式复制,或用 .loc[] 赋值
忽略数据类型内存浪费 + 性能差Pandas 默认用 object 存字符串df.astype({"col": "category"}) 或指定 dtype
大文件直接 read_csv内存溢出一次性加载全部数据chunksize 参数分块,或用 Polars
用 Python 循环替代向量化速度慢 100 倍逐行操作抵消了 NumPy 优势使用 Pandas/NumPy 向量化操作
忘记处理时区时间数据错乱时间戳默认无时区pd.to_datetime(col, utc=True).dt.tz_convert('Asia/Shanghai')
合并后行数暴增数据爆炸多对多合并产生笛卡尔积合并前检查重复键,用 validate='one_to_one'
过早优化图表花大量时间调 matplotlib分析阶段图表是工具不是产品探索阶段用 Seaborn 快速出图,最终报告再精调

陷阱详解:SettingWithCopyWarning

python
# ❌ 反面:修改视图触发警告
subset = df[df["city"] == "Beijing"]  # subset 可能是视图
subset["salary"] = subset["salary"] * 1.1  # Warning!

# ✅ 正面:显式复制
subset = df[df["city"] == "Beijing"].copy()
subset["salary"] = subset["salary"] * 1.1  # 安全

# ✅ 正面:用 .loc[] 直接在原 DataFrame 上修改
df.loc[df["city"] == "Beijing", "salary"] *= 1.1

最佳实践速查表

场景推荐做法避免
数据读取df.info() 检查类型盲目信任数据格式
缺失值根据业务含义选择填充策略统一用 0 或删除填充
大文件chunksize 或 Polars直接 read_csv
向量化优先 NumPy/Pandas 内置操作Python 循环逐行处理
图表探索Seaborn 快速出图一开始就调 matplotlib
数据导出Parquet(更快更小)到处用 CSV
列名规范小写+下划线 user_name混用大小写或空格
链式操作df.query().assign()中间创建大量临时变量
分组聚合agg() 一次多指标多次 groupby

版本说明

版本关键特性影响
Pandas 2.xPyArrow 后端、Copy-on-Write性能大幅提升,需注意写入行为变化
NumPy 2.x更严格的类型提升规则某些隐式类型转换不再生效
Polars 0.20+LazyFrame、多线程大数据集的首选替代方案
Copy-on-Write(Pandas 2.x)

Pandas 2.x 引入了 Copy-on-Write 模式(pd.options.mode.copy_on_write = True),将彻底解决 SettingWithCopyWarning 问题。建议新项目开启此模式。

术语表

术语英文定义
EDAExploratory Data Analysis探索性数据分析:通过统计和可视化理解数据特征
DataFrameDataFramePandas 中二维表格数据结构
SeriesSeriesPandas 中一维数组数据结构
向量化Vectorization用数组运算替代循环,充分利用底层 C 实现
缺失值Missing Value数据中的空值(NaN、None、NA)
异常值Outlier显著偏离大多数数据的极端值
特征工程Feature Engineering从原始数据中创建新变量以改善分析/建模效果
分组聚合GroupBy Aggregation按分类变量分组并计算每组的统计量
IQRInterquartile Range四分位距:Q3 - Q1,用于识别异常值
ParquetApache Parquet列式存储格式,比 CSV 更快更小
Copy-on-WriteCoW写入时复制:Pandas 2.x 的优化策略

延伸阅读

官方文档

推荐阅读

版本差异(数据科学栈 → 当前版本)

本文编写时当前稳定版升级要点
Python3.8-3.123.143.12+ 起性能显著提升;3.14 PEP 649/750
NumPy1.x/2.02.3.xnp.float_ 等别名移除;NEP 50 类型提升
Pandas1.x/2.x3.0.xCopy-on-Write 默认开启;inplace 移除;字符串 dtype 变化
Matplotlib3.x3.x 稳定版API 兼容,样式更新
Seaborn0.12/0.130.13.xAPI 稳定
scikit-learn1.x1.7.xAPI 稳定,新算法持续加入

本文讲解的数据分析流程(读取→清洗→分析→可视化)与核心 API 在最新版本中成立;升级时重点关注 Pandas 3.0 的 Copy-on-Write 与 NumPy 2.x 的类型变化。