写在前面:为什么数据分析现在是每个人的必选项
三年前,数据分析是数据工程师和算法专家的专属领域;三年后,它成了产品经理、市场运营、财务、HR乃至中小企业主的标配技能。原因很简单——数据不会说谎,但它需要有人来”翻译”。
Python之所以成为数据分析的第一语言,不是因为它是最快的语言(R在某些统计场景下更快),也不是因为它的语法最优雅(Julia正在追赶),而是因为它构建了一个从数据采集到数据可视化的完整生态链。你不需要在五个工具之间切换,一个Python环境就能搞定从Excel表格到生产级数据管道的全部工作。
这篇文章的目标不是让你记住Pandas的所有API(那是文档的事),而是帮你建立一套可复用的数据分析工作流——当你拿到一份陌生的数据时,知道该从哪里下手,知道哪些坑必须避开,知道如何让数据说话。
我会用真实的业务场景贯穿全文,每个环节都配有可直接运行的代码。读完之后,你不仅能分析数据,还能用数据讲故事。
第一章:认识你的工具链——NumPy、Pandas、Matplotlib的定位与分工
很多初学者犯的第一个错误是试图用Pandas解决所有问题。Pandas很强,但它不是万能的。理解三个核心库的定位,是高效工作的前提。
1.1 NumPy:数值计算的基石
NumPy是Python科学计算的底层引擎。它提供N维数组对象ndarray,所有后续的数据分析操作(包括Pandas)都建立在NumPy之上。
import numpy as np
# 创建数组
arr = np.array([1, 2, 3, 4, 5])
print(f"一维数组: {arr}")
# 二维数组(矩阵)
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
])
print(f"矩阵形状: {matrix.shape}") # (3, 3)
# NumPy的核心优势:向量化运算
# 不用写循环,整个数组一次性计算
prices = np.array([100, 120, 150, 180, 200])
discount = 0.8
final_prices = prices * discount # [80. 96. 120. 144. 160.]
print(f"折后价格: {final_prices}")
关键认知:NumPy的ndarray是同质数据(所有元素类型相同),这让它能利用底层C语言实现极快的数值计算。当你处理纯数值数据时,优先考虑NumPy;当你需要混合类型(数字、字符串、日期混在一起)和标签化索引时,转向Pandas。
1.2 Pandas:数据分析的主力军
Pandas提供了两个核心数据结构:
- Series:带标签的一维数组
- DataFrame:带标签的二维表格(类似Excel,但强大得多)
import pandas as pd
# 创建DataFrame(最常见的方式:从字典)
data = {
'姓名': ['张三', '李四', '王五', '赵六'],
'年龄': [28, 35, 22, 41],
'部门': ['技术部', '市场部', '技术部', '财务部'],
'薪资': [15000, 20000, 12000, 18000]
}
df = pd.DataFrame(data)
print("原始数据:")
print(df)
print(f"\n数据形状: {df.shape}") # (4, 4) 表示4行4列
print(f"\n数据类型:\n{df.dtypes}")
输出:
原始数据:
姓名 年龄 部门 薪资
0 张三 28 技术部 15000
1 李四 35 市场部 20000
2 王五 22 技术部 12000
3 赵六 41 财务部 18000
数据形状: (4, 4)
数据类型:
姓名 object
年龄 int64
部门 object
薪资 int64
dtype: object
1.3 Matplotlib:数据可视化的画布
Matplotlib是Python最基础的绘图库。Pandas的内置绘图功能底层调用的就是Matplotlib。
import matplotlib.pyplot as plt
# 最简单的柱状图
departments = df['部门'].value_counts()
departments.plot(kind='bar', color=['#4CAF50', '#2196F3', '#FF9800'])
plt.title('各部门人数分布')
plt.xlabel('部门')
plt.ylabel('人数')
plt.tight_layout()
plt.savefig('department_chart.png', dpi=150) # 高清保存
plt.show()
关键认知:Matplotlib学习曲线较陡,但控制粒度极细。对于快速探索性分析,Pandas内置的df.plot()够用;对于 publication-quality 的图表,直接操作Matplotlib。
第二章:数据获取与加载——从Excel到数据库的全链路
真实世界的数据不会乖乖地以规范格式出现在你面前。它可能藏在Excel表格的某个Sheet里,可能躺在CSV的乱码编码中,可能分散在多个系统的数据库里。本章解决”数据怎么来”的问题。
2.1 加载本地文件
import pandas as pd
# Excel文件(多Sheet)
df_excel = pd.read_excel('sales_data.xlsx', sheet_name='2024Q1')
# CSV文件(注意编码问题)
df_csv = pd.read_csv('data.csv', encoding='utf-8')
# 如果默认编码报错,尝试:
df_csv = pd.read_csv('data.csv', encoding='gbk') # 中文Windows常见
# 指定列类型(避免Pandas自动推断出错)
df = pd.read_csv('data.csv', dtype={
'用户ID': str, # 防止长数字被科学计数法截断
'订单金额': float,
'订单日期': 'datetime64[ns]'
})
# 只读取前1000行(大数据文件加速)
df_sample = pd.read_csv('huge_data.csv', nrows=1000)
# 跳过某些行
df_skip = pd.read_csv('data.csv', skiprows=[2, 3, 5]) # 跳过第2、3、5行
2.2 连接数据库
# 连接MySQL
import pymysql
from sqlalchemy import create_engine
engine = create_engine('mysql+pymysql://username:password@host:port/dbname')
# 直接读取SQL查询结果
df = pd.read_sql('SELECT * FROM orders WHERE create_time > "2024-01-01"', engine)
# 连接PostgreSQL
import psycopg2
engine_pg = create_engine('postgresql://username:password@host:port/dbname')
df_pg = pd.read_sql('SELECT * FROM users', engine_pg)
2.3 处理API数据
import requests
import json
# 调用API获取数据
response = requests.get('https://api.example.com/data', params={
'page': 1,
'limit': 100
})
data = response.json()
# 转为DataFrame
df_api = pd.DataFrame(data['items'])
# 处理嵌套JSON(如返回的是层级结构)
nested_data = [
{'user_id': 1, 'orders': [{'order_id': 'A001', 'amount': 100}, {'order_id': 'A002', 'amount': 200}]},
{'user_id': 2, 'orders': [{'order_id': 'B001', 'amount': 150}]}
]
df_nested = pd.json_normalize(nested_data, record_path='orders', meta=['user_id'])
print(df_nested)
输出:
order_id amount user_id
0 A001 100 1
1 A002 200 1
2 B001 150 2
第三章:数据清洗——80%的时间花在这里
数据清洗是数据分析中最枯燥但最重要的环节。Garbage in, garbage out——如果输入的数据质量差,后续所有分析结果都是误导性的。
3.1 缺失值处理
import pandas as pd
import numpy as np
# 模拟含缺失值的数据
df = pd.DataFrame({
'姓名': ['张三', '李四', None, '赵六', '钱七'],
'年龄': [28, None, 22, 41, np.nan],
'薪资': [15000, 20000, 12000, None, 18000],
'部门': ['技术部', '市场部', '技术部', '财务部', '技术部']
})
print("原始数据缺失情况:")
print(df.isnull().sum())
print(f"\n缺失比例:\n{df.isnull().mean():.2%}")
输出:
原始数据缺失情况:
姓名 1
年龄 2
薪资 1
部门 0
dtype: int64
缺失比例:
姓名 20.00%
年龄 40.00%
薪资 20.00%
部门 0.00%
dtype: float64
处理策略选择:
# 策略1:删除缺失值(适合缺失比例<5%且随机缺失)
df_drop = df.dropna()
# 策略2:填充缺失值(更常用)
# 数值型:用均值/中位数/众数填充
df['年龄'] = df['年龄'].fillna(df['年龄'].median()) # 中位数抗异常值
df['薪资'] = df['薪资'].fillna(df['薪资'].mean())
# 分类型:用众数填充
df['姓名'] = df['姓名'].fillna(df['姓名'].mode()[0])
# 策略3:向前/向后填充(适合时间序列)
df['薪资'] = df['薪资'].fillna(method='ffill') # 前向填充
df['薪资'] = df['薪资'].fillna(method='bfill') # 后向填充
# 策略4:智能填充(用其他字段预测)
# 比如:根据部门填充年龄
df['年龄'] = df.groupby('部门')['年龄'].transform(
lambda x: x.fillna(x.median())
)
# 策略5:标记为"缺失"(有些业务场景缺失本身就有意义)
df['年龄_缺失标记'] = df['年龄'].isnull().astype(int)
3.2 重复值处理
# 检测重复
print(f"重复行数: {df.duplicated().sum()}")
# 删除重复(保留第一次出现的)
df_clean = df.drop_duplicates()
# 删除重复(基于特定列)
df_clean = df.drop_duplicates(subset=['姓名', '部门'])
# 只保留最后一次出现
df_clean = df.drop_duplicates(keep='last')
3.3 类型转换
# 查看当前类型
print(df.dtypes)
# 转换日期类型(最常见的问题:Pandas把日期读成字符串)
df['订单日期'] = pd.to_datetime(df['订单日期'], format='%Y/%m/%d')
# 提取日期组件
df['年份'] = df['订单日期'].dt.year
df['月份'] = df['订单日期'].dt.month
df['季度'] = df['订单日期'].dt.quarter
df['星期'] = df['订单日期'].dt.day_name() # 周一、周二...
# 转换数值类型
df['年龄'] = df['年龄'].astype('Int64') # 可空整数(Pandas新类型)
df['薪资'] = df['薪资'].astype(float)
# 转换分类类型(节省内存,加速分析)
df['部门'] = df['部门'].astype('category')
print(f"部门类型: {df['部门'].dtype}") # category
3.4 异常值检测与处理
# 方法1:IQR(四分位距)法——最常用
Q1 = df['薪资'].quantile(0.25)
Q3 = df['薪资'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print(f"薪资异常值范围: [{lower_bound:.0f}, {upper_bound:.0f}]")
# 标记异常值
df['薪资异常'] = (df['薪资'] < lower_bound) | (df['薪资'] > upper_bound)
# 处理异常值:截断(winsorize)
df['薪资_截断'] = df['薪资'].clip(lower=lower_bound, upper=upper_bound)
# 方法2:Z-score法(适合正态分布数据)
from scipy import stats
z_scores = np.abs(stats.zscore(df['薪资'].dropna()))
df['薪资异常_z'] = z_scores > 3 # Z-score > 3 视为异常
# 方法3:业务规则(最可靠)
# 比如:年龄不可能<0或>150
df = df[(df['年龄'] >= 0) & (df['年龄'] <= 150)]
3.5 字符串清洗
# 去除空格
df['姓名'] = df['姓名'].str.strip()
# 统一大小写
df['部门'] = df['部门'].str.lower()
# 替换文本
df['邮箱'] = df['邮箱'].str.replace('@company', '@corp.com')
# 提取子串
df['区号'] = df['电话'].str[:3] # 假设电话格式为 010-12345678
# 正则表达式匹配
df['手机号'] = df['联系方式'].str.extract(r'1[3-9]\d{9}')
# 分割字符串
df[['姓', '名']] = df['姓名'].str.split('', n=1, expand=True)
第四章:数据探索与分析——让数据开口说话
清洗完毕的数据终于可以用了。这一步的目标是理解数据的分布、关系和规律,为后续建模打基础。
4.1 描述性统计
# 基础统计
print(df.describe())
# 输出示例:
# 年龄 薪资
# count 5.00 5.00
# mean 31.20 17000.00
# std 7.43 3162.28
# min 22.00 12000.00
# 25% 28.00 15000.00
# 50% 35.00 18000.00
# 75% 41.00 20000.00
# max 41.00 20000.00
# 更详细的统计
print(df.describe(include='all')) # 包含分类变量
# 自定义统计
print(f"年龄中位数: {df['年龄'].median()}")
print(f"薪资偏度: {df['薪资'].skew():.2f}") # 偏度>0表示右偏(有高薪 outlier)
print(f"薪资峰度: {df['薪资'].kurtosis():.2f}") # 峰度>0表示尖峰厚尾
4.2 分组聚合
# 按部门统计
dept_stats = df.groupby('部门').agg({
'年龄': ['mean', 'median', 'count'],
'薪资': ['mean', 'sum', 'max']
}).round(2)
print(dept_stats)
# 多字段分组
monthly_sales = df.groupby(['部门', '季度'])['薪资'].sum().unstack()
print(monthly_sales)
# pivot_table(透视表)
pivot = df.pivot_table(
values='薪资',
index='部门',
columns='季度',
aggfunc='mean',
margins=True, # 显示合计行/列
margins_name='总计'
)
print(pivot)
输出示例:
季度 Q1 Q2 Q3 Q4 总计
部门
财务部 18000 18500 19000 19500 18750
市场部 20000 21000 20500 21500 20750
技术部 15000 15500 16000 16500 15750
总计 16667 17167 17333 17833 17250
4.3 交叉分析
”`python
交叉表(Crosstab)
ct = pd.crosstab(df[‘部门’], df[‘年龄分组’], margins=True) print(ct)
条件概率
pt = pd.crosstab(df[‘部门’], df[‘薪资分组’], normalize=’
