Python数据分析实战:从Pandas基础到Scrapy爬虫完整指南
Python数据分析实战:从Pandas基础到Scrapy爬虫完整指南
你是否也曾被“数据分析”这个词吸引,兴致勃勃地学完了Pandas的基础操作,却对着空白的Jupyter Notebook发呆,不知道如何将这些零散的知识点串联起来,应用到真实世界的问题中?或者,你尝试过从网上抓取数据,但面对杂乱无章的网页结构,清洗和整合数据的过程让你望而却步。这正是许多初学者从理论迈向实践时遇到的最大鸿沟——“学完基础,却不知如何应用”。
这篇文章正是为你准备的。我们将彻底抛弃枯燥的考试提纲和孤立的语法讲解,转而构建一个 “数据采集→清洗→分析→可视化” 的完整实战闭环。我们将以电商评论分析和股票数据洞察这两个极具代表性的场景为线索,手把手带你将Pandas的数据处理能力与Scrapy爬虫框架的采集能力深度融合。你会发现,数据分析不再是空中楼阁,而是一套可以解决实际问题的、有章可循的“组合拳”。无论你是希望提升业务洞察力的市场人员,还是渴望用数据驱动决策的开发者,这套从基础到实战的完整指南,都将为你铺平道路。
1. 构建数据分析的基石:深入理解Pandas核心数据结构
在开始我们的实战之旅前,我们必须先夯实基础。Pandas之所以成为Python数据分析的事实标准,核心在于其两个强大的数据结构:Series和DataFrame。很多人把它们简单理解为“一维数组”和“二维表格”,但这远远不够。理解它们的内在逻辑和设计哲学,才能让你在后续操作中游刃有余。
Series 不仅仅是一个带标签的数组。你可以把它想象成一个固定长度、有序的字典。它的索引(index)是键,数据(values)是值,但比字典更强大的是,它支持向量化操作和广播机制。而 DataFrame 则是多个共享同一个索引的Series的集合,它更像一个关系型数据库中的表,或者Excel中的一个工作表,是进行大多数数据分析任务的主战场。
让我们通过一个简单的例子,快速感受一下它们的创建与基本操作:
import pandas as pd
import numpy as np
# 创建Series:从列表、字典或NumPy数组
scores = pd.Series([88, 92, 79, 95], index=['Alice', 'Bob', 'Charlie', 'Diana'], name='期末成绩')
print(scores)
这段代码会输出一个带有姓名索引的成绩序列。Series的索引赋予了数据明确的语义,这是区别于普通列表的关键。
接下来是DataFrame的创建,这是更常见的起点:
# 创建DataFrame:从字典的列表
data = [
{'姓名': 'Alice', '部门': '市场部', '销售额': 150000, '客户数': 45},
{'姓名': 'Bob', '部门': '技术部', '销售额': 80000, '客户数': 12},
{'姓名': 'Charlie', '部门': '市场部', '销售额': 220000, '客户数': 67},
{'姓名': 'Diana', '部门': '销售部', '销售额': 190000, '客户数': 58}
]
df_sales = pd.DataFrame(data)
print(df_sales)
现在,我们有了一个包含员工销售信息的表格。仅仅创建出来还不够,我们需要熟练地访问和操作它。
1.1 数据的选择与切片:像操作Excel一样自如
Pandas提供了多种灵活的数据选取方式,我习惯将它们分为两类:基于标签的索引(loc)和基于位置的索引(iloc)。这是新手最容易混淆,但也是必须掌握的核心。
df.loc[]: 通过行和列的标签名进行选择。它包含结束位置。df.iloc[]: 通过行和列的整数位置(从0开始)进行选择。它不包含结束位置,和Python列表切片一致。
看一个对比表格就一目了然了:
| 操作 | 代码示例 | 结果说明 |
|---|---|---|
| 选择单列 | df_sales[‘销售额’] 或 df_sales.销售额 | 返回一个Series |
| 选择多列 | df_sales[[‘姓名’, ‘销售额’]] | 返回一个只包含指定列的新DataFrame |
用loc选择行 | df_sales.loc[0] | 选择索引为0的行(返回Series) |
用loc选择行列 | df_sales.loc[0, ‘姓名’] | 选择索引0行,‘姓名’列的值(‘Alice’) |
用loc切片行 | df_sales.loc[0:2] | 选择索引从0到2的所有行(包含2) |
用iloc切片行 | df_sales.iloc[0:2] | 选择位置从0到1的行(不包含2) |
用iloc选择行列 | df_sales.iloc[1, 2] | 选择第1行(位置),第2列(位置)的值(80000) |
提示:在日常使用中,如果索引是有意义的标签(如员工ID、日期),优先使用
loc,代码可读性更强。如果只是需要按位置操作,则用iloc。避免在代码中混用,以免造成逻辑混乱。
1.2 核心数据处理方法:清洗与转换的利器
拥有了数据,下一步就是整理。原始数据几乎总是“脏”的——存在缺失值、重复项、格式不一致等问题。Pandas提供了一系列方法来进行数据清洗。
处理缺失值是第一步。我们可以用isnull()或notnull()来检测缺失值,然后用dropna()删除或fillna()填充。
# 假设我们的数据中‘客户数’有缺失
df_sales.loc[1, ‘客户数’] = np.nan
print(df_sales.isnull().sum()) # 查看每列缺失值数量
# 策略1:删除缺失行(谨慎使用,可能丢失数据)
df_dropped = df_sales.dropna()
# 策略2:填充缺失值(例如用列均值填充)
df_filled = df_sales.fillna({'客户数': df_sales['客户数'].mean()})
数据转换同样重要。例如,我们想计算每个人的“单客贡献额”:
df_sales[‘单客贡献’] = df_sales[‘销售额’] / df_sales[‘客户数’]
这里就用到了Pandas的广播特性,整个列的计算无需循环,一行代码即可完成,效率极高。
此外,分组聚合(groupby)是数据分析的灵魂。比如,我们想计算每个部门的平均销售额和总客户数:
dept_stats = df_sales.groupby(‘部门’).agg({
‘销售额’: ‘mean’,
‘客户数’: ‘sum’,
‘姓名’: ‘count’ # 统计部门人数
}).rename(columns={‘姓名’: ‘人数’, ‘销售额’: ‘平均销售额’})
print(dept_stats)
groupby 与 agg(聚合)的组合,能让我们轻松实现SQL中的GROUP BY功能,是进行多维数据透视的基石。掌握了这些,你的Pandas基础就已经相当扎实了。接下来,我们将解决数据的源头问题——如何获取数据。
2. 从互联网获取鲜活数据:Scrapy爬虫框架实战入门
数据分析的原料是数据。而在互联网时代,大量有价值的信息存在于网页中。Scrapy是一个为爬取网站数据、提取结构化数据而生的强大、快速的Python框架。它采用异步处理,效率远高于简单的requests+BeautifulSoup组合,并且内置了完善的中间件、管道(Pipeline)系统,非常适合构建复杂、健壮的爬虫项目。
与直接写脚本抓取不同,Scrapy要求你以项目的形式组织代码,这虽然初期有点繁琐,但对于管理多个爬虫、处理复杂逻辑来说,是最佳实践。让我们从创建一个爬虫项目开始。
2.1 搭建第一个Scrapy爬虫项目
首先,确保安装了Scrapy:pip install scrapy。然后,我们创建一个名为product_review的项目,目标是爬取某个电商平台的商品评论。
scrapy startproject product_review
cd product_review
scrapy genspider example example.com
这个命令会生成一个标准的项目结构。关键文件是:
items.py: 定义你要爬取的数据结构(类似于模型)。pipelines.py: 处理爬取到的数据(如清洗、存入数据库)。middlewares.py: 自定义中间件,用于处理请求和响应。settings.py: 项目设置,如并发数、下载延迟、启用哪些组件。spiders/目录: 存放爬虫文件,每个爬虫定义一个抓取规则。
假设我们要爬取一个书籍信息网站,我们先在items.py中定义数据结构:
import scrapy
class BookItem(scrapy.Item):
# 定义字段,像定义字典的键
title = scrapy.Field() # 书名
price = scrapy.Field() # 价格
rating = scrapy.Field() # 评分
review_count = scrapy.Field() # 评论数
url = scrapy.Field() # 商品链接
2.2 编写爬虫核心:Spider逻辑
接下来,在spiders目录下,我们编写具体的爬虫逻辑。一个基本的Spider需要定义name(唯一标识)、start_urls(起始网址)和parse方法(用于解析响应)。
import scrapy
from product_review.items import BookItem
class BookSpider(scrapy.Spider):
name = ‘books’ # 爬虫名,运行爬虫时使用:scrapy crawl books
allowed_domains = [‘books.toscrape.com’] # 限制爬取的域名
start_urls = [‘http://books.toscrape.com/’]
def parse(self, response):
# 解析列表页,提取每本书的链接
book_links = response.css(‘article.product_pod h3 a::attr(href)’).getall()
for link in book_links:
# 构建绝对URL并跟进,用parse_book回调函数处理详情页
yield response.follow(link, callback=self.parse_book)
# 处理分页:查找‘下一页’的链接
next_page = response.css(‘li.next a::attr(href)’).get()
if next_page:
yield response.follow(next_page, callback=self.parse)
def parse_book(self, response):
# 解析书籍详情页,提取数据并生成Item
item = BookItem()
item[‘title’] = response.css(‘div.product_main h1::text’).get()
item[‘price’] = response.css(‘p.price_color::text’).get()
item[‘rating’] = response.css(‘p.star-rating::attr(class)’).re_first(r’star-rating (\w+)’)
item[‘review_count’] = response.css(‘div.product_main p.star-rating + a::text’).re_first(r’(\d+)’)
item[‘url’] = response.url
yield item # 将数据项抛出,交给Pipeline处理
在这个Spider中,我们使用了CSS选择器来定位元素,代码非常直观。parse方法负责抓取列表页的所有书籍链接,并递归跟进分页。parse_book方法则负责解析每个详情页,填充我们之前定义的BookItem,然后通过yield提交。
注意:在实际项目中,务必遵守网站的
robots.txt协议,并合理设置DOWNLOAD_DELAY(下载延迟),避免对目标网站造成过大压力,这也是基本的网络礼仪。
2.3 数据存储与后续处理:Pipeline的妙用
爬取到的数据需要保存。Scrapy的Pipeline组件就是为此而生。我们可以在pipelines.py中编写逻辑,将数据保存为CSV、JSON文件,或者存入数据库。
一个简单的将数据保存为JSON行的Pipeline如下:
import json
class JsonWriterPipeline:
def open_spider(self, spider):
# 爬虫启动时打开文件
self.file = open(‘books.jl’, ‘w’, encoding=‘utf-8’)
def close_spider(self, spider):
# 爬虫关闭时关闭文件
self.file.close()
def process_item(self, item, spider):
# 处理每个item,写入一行JSON
line = json.dumps(dict(item), ensure_ascii=False) + “\n”
self.file.write(line)
return item # 必须返回item,以便其他Pipeline可以继续处理
别忘了在settings.py中启用这个Pipeline,并设置其优先级:
ITEM_PIPELINES = {
‘product_review.pipelines.JsonWriterPipeline’: 300,
}
现在,运行爬虫:scrapy crawl books -o books.csv。-o参数是Scrapy自带的快捷导出功能,但我们自定义的Pipeline提供了更灵活的处理方式。爬取结束后,你就获得了一个包含所有书籍信息的books.jl文件(每行一个JSON对象)。这正是Pandas可以完美读取的格式。至此,数据的“采”环节已完成,接下来进入“洗”和“析”的环节。
3. 实战案例一:电商评论情感分析与业务洞察
现在,我们拥有了原始的爬虫数据(假设我们爬取的是真实电商平台的评论数据),让我们将其加载到Pandas中,完成一个完整的数据分析案例:电商商品评论情感分析与业务洞察。
假设我们爬取到的评论数据reviews.jl包含字段:product_id, user_name, rating, comment, date。
3.1 数据加载与初步探索
首先,用Pandas读取数据并进行初步探索性数据分析(EDA)。
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use(‘seaborn-v0_8-darkgrid’) # 设置绘图样式
# 读取数据
df_reviews = pd.read_json(‘reviews.jl’, lines=True)
print(f“数据形状: {df_reviews.shape}”)
print(df_reviews.head())
print(df_reviews.info())
print(df_reviews[‘rating’].describe())
df.info()可以快速查看数据类型和缺失情况,df.describe()则对数值列进行统计摘要。我们可能立刻发现一些问题,比如comment字段有缺失,rating的分布如何。
3.2 数据清洗与预处理
这是将“脏数据”变为“干净数据”的关键步骤。
- 处理缺失值:评论内容缺失,我们可以选择删除,或者用空字符串填充,这取决于后续分析是否需要文本。
df_reviews = df_reviews.dropna(subset=[‘rating’]) # 评分是核心,不能缺失 df_reviews[‘comment’] = df_reviews[‘comment’].fillna(‘’) # 评论缺失则填空字符串 - 格式标准化:确保
date是日期时间类型,rating是数值型。df_reviews[‘date’] = pd.to_datetime(df_reviews[‘date’]) df_reviews[‘rating’] = pd.to_numeric(df_reviews[‘rating’], errors=‘coerce’) - 异常值处理:检查评分是否在合理范围内(如1-5分)。
df_reviews = df_reviews[(df_reviews[‘rating’] >= 1) & (df_reviews[‘rating’] <= 5)] - 文本预处理(为情感分析准备):这是一个更深入的步骤,可能包括去除特殊字符、分词、去除停用词等。这里我们做一个简单的关键词提取示例。
# 简单的情感倾向判断:根据评分 df_reviews[‘sentiment’] = df_reviews[‘rating’].apply(lambda x: ‘positive’ if x >= 4 else (‘negative’ if x <= 2 else ‘neutral’))
3.3 数据分析与可视化
现在,我们可以开始提出业务问题并寻找答案。
问题1:产品的整体口碑如何?评分分布怎样?
# 评分分布直方图
plt.figure(figsize=(10, 6))
rating_counts = df_reviews[‘rating’].value_counts().sort_index()
ax = sns.barplot(x=rating_counts.index, y=rating_counts.values, palette=‘viridis’)
plt.title(‘商品评分分布’, fontsize=15)
plt.xlabel(‘评分’)
plt.ylabel(‘评论数量’)
for i, v in enumerate(rating_counts.values):
ax.text(i, v+5, str(v), ha=‘center’)
plt.tight_layout()
plt.show()
# 计算平均分和情感比例
avg_rating = df_reviews[‘rating’].mean()
sentiment_dist = df_reviews[‘sentiment’].value_counts(normalize=True)
print(f“平均评分: {avg_rating:.2f}”)
print(“情感分布:”)
print(sentiment_dist)
问题2:评论数量随时间的变化趋势如何?是否有某个时间段口碑爆发或下滑?
# 按月份聚合评论数量和平均分
df_reviews[‘month’] = df_reviews[‘date’].dt.to_period(‘M’)
monthly_stats = df_reviews.groupby(‘month’).agg(
review_count=(‘rating’, ‘count’),
avg_rating=(‘rating’, ‘mean’)
).reset_index()
monthly_stats[‘month’] = monthly_stats[‘month’].dt.to_timestamp()
# 绘制双轴折线图
fig, ax1 = plt.subplots(figsize=(14, 7))
ax2 = ax1.twinx()
ax1.plot(monthly_stats[‘month’], monthly_stats[‘review_count’], color=‘blue’, marker=‘o’, label=‘评论量’)
ax1.set_xlabel(‘月份’)
ax1.set_ylabel(‘评论数量’, color=‘blue’)
ax1.tick_params(axis=‘y’, labelcolor=‘blue’)
ax2.plot(monthly_stats[‘month’], monthly_stats[‘avg_rating’], color=‘red’, marker=‘s’, label=‘平均评分’)
ax2.set_ylabel(‘平均评分’, color=‘red’)
ax2.tick_params(axis=‘y’, labelcolor=‘red’)
plt.title(‘评论数量与平均评分月度趋势’, fontsize=15)
fig.legend(loc=“upper left”, bbox_to_anchor=(0.1, 0.9))
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
通过这个分析,运营团队可以清晰地看到产品口碑随时间的变化,关联市场活动,评估其效果。例如,如果发现某次促销后评论量激增但平均分下降,可能需要反思促销吸引的用户群体或产品质量是否出现问题。
4. 实战案例二:整合金融市场数据与可视化
第二个案例,我们将目光投向金融市场。这里的数据源更加结构化,通常来自公开的API(如雅虎财经、聚宽)或财经数据平台。我们将演示如何获取、处理并可视化股票数据,进行简单的技术分析。
4.1 使用Pandas获取金融时间序列数据
Pandas自身就集成了强大的金融数据获取能力,通过pandas-datareader库(需单独安装)可以方便地获取雅虎财经等数据源的历史数据。
import pandas_datareader.data as web
import datetime
# 定义时间范围
start = datetime.datetime(2023, 1, 1)
end = datetime.datetime(2023, 12, 31)
# 获取苹果公司股票数据
try:
# 注意:雅虎财经的API可能有变化,此处为示例
df_apple = web.DataReader(‘AAPL’, ‘stooq’, start, end) # 使用stooq数据源
print(df_apple.head())
except Exception as e:
print(f“数据获取失败: {e}”)
# 模拟一些数据用于演示
dates = pd.date_range(start, end, freq=‘B’)
df_apple = pd.DataFrame({
‘Close’: np.cumsum(np.random.randn(len(dates)) * 10 + 180),
‘Volume’: np.random.randint(1e6, 5e7, size=len(dates))
}, index=dates)
获取到的数据通常包含开盘价(Open)、最高价(High)、最低价(Low)、收盘价(Close)、成交量(Volume)等,索引是日期。
4.2 金融数据分析与特征工程
对于时间序列数据,我们经常需要计算一些衍生指标,如收益率、移动平均线等。
# 计算日收益率(今日收盘价相对于昨日收盘价的变化率)
df_apple[‘Daily_Return’] = df_apple[‘Close’].pct_change()
# 计算简单移动平均线(SMA),常用窗口为20日和50日
df_apple[‘SMA_20’] = df_apple[‘Close’].rolling(window=20).mean()
df_apple[‘SMA_50’] = df_apple[‘Close’].rolling(window=50).mean()
# 计算波动率(以20日滚动标准差衡量)
df_apple[‘Volatility_20’] = df_apple[‘Daily_Return’].rolling(window=20).std() * np.sqrt(252) # 年化
print(df_apple[[‘Close’, ‘Daily_Return’, ‘SMA_20’, ‘SMA_50’]].tail())
4.3 多维度可视化与洞察
金融数据的可视化能直观揭示趋势、波动和关系。
fig, axes = plt.subplots(3, 1, figsize=(15, 12), sharex=True)
# 子图1:价格与移动平均线
axes[0].plot(df_apple.index, df_apple[‘Close’], label=‘收盘价’, linewidth=1, alpha=0.7)
axes[0].plot(df_apple.index, df_apple[‘SMA_20’], label=‘20日SMA’, linewidth=2)
axes[0].plot(df_apple.index, df_apple[‘SMA_50’], label=‘50日SMA’, linewidth=2)
axes[0].set_ylabel(‘价格’)
axes[0].set_title(‘AAPL股价与移动平均线’)
axes[0].legend()
axes[0].grid(True, alpha=0.3)
# 子图2:日收益率分布
axes[1].hist(df_apple[‘Daily_Return’].dropna(), bins=50, edgecolor=‘black’, alpha=0.7, density=True)
axes[1].axvline(df_apple[‘Daily_Return’].mean(), color=‘red’, linestyle=‘--’, label=f”均值: {df_apple[‘Daily_Return’].mean():.4f}“)
axes[1].set_xlabel(‘日收益率’)
axes[1].set_ylabel(‘频率’)
axes[1].set_title(‘日收益率分布’)
axes[1].legend()
# 子图3:成交量
axes[2].bar(df_apple.index, df_apple[‘Volume’] / 1e6, width=1, alpha=0.6, color=‘gray’)
axes[2].set_xlabel(‘日期’)
axes[2].set_ylabel(‘成交量 (百万)’)
axes[2].set_title(‘成交量’)
axes[2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
第一个图表展示了价格趋势和均线关系,当短期均线上穿长期均线时,常被视为买入信号(金叉),反之则为卖出信号(死叉)。第二个图表展示了收益率的分布,有助于评估风险(波动性)。第三个图表是成交量,价格变动伴随高成交量通常意味着趋势更强。
更进一步,我们可以分析不同资产之间的相关性:
# 假设我们还有微软和谷歌的股票数据
# df_msft, df_goog 已获取
combined_df = pd.DataFrame({
‘AAPL’: df_apple[‘Daily_Return’],
‘MSFT’: df_msft[‘Daily_Return’],
‘GOOG’: df_goog[‘Daily_Return’]
}).dropna()
# 计算相关系数矩阵
corr_matrix = combined_df.corr()
print(“收益率相关系数矩阵:”)
print(corr_matrix)
# 绘制热力图
plt.figure(figsize=(8, 6))
sns.heatmap(corr_matrix, annot=True, cmap=‘coolwarm’, center=0, square=True)
plt.title(‘股票日收益率相关系数热力图’)
plt.tight_layout()
plt.show()
这张热力图能清晰展示哪些股票走势趋同(正相关),哪些走势相反(负相关),对于构建投资组合、分散风险至关重要。
从Scrapy爬虫抓取原始数据,到用Pandas进行深度清洗、转换、分析与可视化,我们完整地走通了两条极具代表性的实战路径。你会发现,工具本身并不复杂,真正的挑战和乐趣在于如何将工具组合起来,针对具体问题设计分析流程,并从数据中提炼出有意义的见解。这套“采集-处理-分析”的思维框架,远比死记硬背某个函数的参数更有价值。当你下次面对一个新的数据集时,不妨先问自己:数据从哪来?干净吗?我想回答什么问题?然后,沿着这个指南提供的路径,一步步探索下去。
更多推荐
所有评论(0)