Python数据分析进阶课程从数据清洗到可视化实战帮你解决数据处理难题
走进数据的世界,从混乱到清晰
说实话,刚接触数据分析的时候,我也曾经被那些杂乱无章的数据折磨得够呛。打开一个CSV文件,里面有的字段全是空值,有的日期格式乱七八糟,还有的列名长得像密码一样让人摸不着头脑。那时候我就在想,要是能有人手把手教我怎么把这些数据整理得明明白白,该多好。
今天这篇文章,就是想把你从那些”数据泥潭”里拉出来,一步步带你走过数据清洗、处理和可视化的完整旅程。不用担心,我会用最通俗的语言,配合实际的代码例子,让你真正理解每一个步骤在做什么。
数据清洗:给数据”洗澡”
数据清洗这一步,可以说是整个数据分析中最关键也最容易被忽视的环节。很多人急着去做分析,结果发现分析出来的结果完全不对劲,回头一查,原来是数据本身就有问题。
认识你的数据
在动手清洗之前,你得先了解手上有什么。假设你拿到了一份电商销售数据,里面包含了订单ID、用户信息、商品详情、交易时间等等。
import pandas as pd
import numpy as np
# 读取数据
df = pd.read_csv('sales_data.csv')
# 快速了解数据概况
print(df.shape) # 查看数据有多少行多少列
print(df.head()) # 查看前5行数据
print(df.dtypes) # 查看每一列的数据类型
print(df.info()) # 查看数据详细信息
print(df.describe()) # 查看数值列的统计信息
通过这几行代码,你就能对数据有个基本的认识。你会发现有些列是数字,有些是文本,还有些可能是日期格式。更重要的是,你能看到数据中是否存在缺失值。
处理缺失值
缺失值处理是数据清洗的第一步,也是最常见的问题。数据缺失可能有很多种原因:用户没填、系统出错、数据导出时丢失等等。
# 查看缺失值情况
print(df.isnull().sum()) # 查看每一列有多少缺失值
print(df.isnull().mean()) # 查看缺失值比例
处理缺失值有几种常见策略:
第一种:删除 如果某一行数据缺失太多,或者缺失值占比很小,可以直接删除。
# 删除缺失值超过50%的列
df = df.dropna(thresh=len(df)*0.5, axis=1)
# 删除缺失值超过30%的行
df = df.dropna(thresh=len(df.columns)*0.7, axis=0)
第二种:填充 对于数值型数据,可以用均值、中位数或众数填充;对于分类数据,可以用众数填充。
# 数值型用中位数填充
df['age'] = df['age'].fillna(df['age'].median())
# 分类数据用众数填充
df['category'] = df['category'].fillna(df['category'].mode()[0])
# 用前后值填充(时间序列常用)
df['value'] = df['value'].fillna(method='ffill') # 前向填充
df['value'] = df['value'].fillna(method='bfill') # 后向填充
第三种:插值法 对于时间序列数据,插值法是个很好的选择。
# 线性插值
df['value'] = df['value'].interpolate(method='linear')
# 时间感知插值
df = df.set_index('date')
df['value'] = df['value'].interpolate(method='time')
df = df.reset_index()
处理重复值
重复数据会严重影响分析结果,必须仔细处理。
# 查看重复行
print(df.duplicated().sum())
# 删除完全重复的行
df = df.drop_duplicates()
# 基于特定列删除重复
df = df.drop_duplicates(subset=['user_id', 'order_date'])
数据类型转换
很多时候,数据加载后类型不对,会导致后续分析出错。
# 转换日期类型
df['order_date'] = pd.to_datetime(df['order_date'])
# 转换数值类型
df['amount'] = pd.to_numeric(df['amount'], errors='coerce')
# 转换类别类型(节省内存)
df['category'] = df['category'].astype('category')
处理异常值
异常值是那些明显偏离正常范围的数据点,可能是录入错误,也可能是真正的极端情况。
# 使用IQR方法检测异常值
Q1 = df['amount'].quantile(0.25)
Q3 = df['amount'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 标记异常值
df['is_outlier'] = (df['amount'] < lower_bound) | (df['amount'] > upper_bound)
# 处理异常值:裁剪到合理范围
df['amount_capped'] = df['amount'].clip(lower=lower_bound, upper=upper_bound)
格式统一和标准化
字段值的格式不统一是很常见的问题,比如”北京”、”北京市”、”北京 市”应该算是同一个城市。
# 去除空格
df['city'] = df['city'].str.strip()
# 统一大小写
df['category'] = df['category'].str.lower()
# 替换常见变体
df['city'] = df['city'].replace({
'北京': '北京市',
'上海': '上海市',
'广州': '广州市'
})
# 标准化日期格式
df['order_date'] = pd.to_datetime(df['order_date'], format='%Y/%m/%d')
数据探索与特征工程
数据清洗完之后,就可以开始探索数据了。这一步的目的是发现数据中的规律和模式,同时为后续分析准备好合适的特征。
探索性数据分析(EDA)
# 数值变量统计
numeric_cols = df.select_dtypes(include=[np.number]).columns
print(df[numeric_cols].describe())
# 分类变量统计
categorical_cols = df.select_dtypes(include=['object', 'category']).columns
for col in categorical_cols:
print(f"\n{col}:")
print(df[col].value_counts())
创建新特征
有时候,原始数据中的字段并不能直接满足分析需求,需要创建新的特征。
# 从日期中提取信息
df['year'] = df['order_date'].dt.year
df['month'] = df['order_date'].dt.month
df['day_of_week'] = df['order_date'].dt.dayofweek
df['quarter'] = df['order_date'].dt.quarter
# 创建派生字段
df['order_amount_per_item'] = df['amount'] / df['quantity']
df['is_high_value'] = df['amount'] > df['amount'].quantile(0.9)
# 时间间隔计算
df['days_since_last_order'] = df['order_date'].diff().dt.days
数据可视化:让数据”说话”
经过清洗和探索,数据已经变得干净整齐了。但光看表格还不够直观,我们需要用可视化来揭示数据背后的故事。
基础图表:matplotlib
matplotlib是最基础的绘图库,虽然学习曲线有点陡峭,但掌握它之后,其他高级库用起来会更顺手。
import matplotlib.pyplot as plt
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 创建子图
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 销售额分布直方图
axes[0, 0].hist(df['amount'], bins=30, color='steelblue', edgecolor='white')
axes[0, 0].set_title('销售额分布')
axes[0, 0].set_xlabel('金额')
axes[0, 0].set_ylabel('频数')
# 月度销售额趋势
monthly_sales = df.groupby(df['order_date'].dt.month)['amount'].sum()
axes[0, 1].plot(monthly_sales.index, monthly_sales.values, marker='o', color='coral')
axes[0, 1].set_title('月度销售额趋势')
axes[0, 1].set_xlabel('月份')
axes[0, 1].set_ylabel('销售额')
# 类别占比饼图
category_sales = df.groupby('category')['amount'].sum().sort_values(ascending=False)
axes[1, 0].pie(category_sales.values, labels=category_sales.index, autopct='%1.1f%%')
axes[1, 0].set_title('各类别销售额占比')
# 城市销售额散点图
axes[1, 1].scatter(df['user_age'], df['amount'], alpha=0.5, color='green')
axes[1, 1].set_title('用户年龄与销售额关系')
axes[1, 1].set_xlabel('年龄')
axes[1, 1].set_ylabel('销售额')
plt.tight_layout()
plt.savefig('exploration_plots.png', dpi=150)
plt.show()
高级可视化:seaborn
seaborn基于matplotlib,但提供了更简洁的API和更美观的默认样式。
import seaborn as sns
# 设置样式
sns.set_style("whitegrid")
sns.set_palette("husl")
# 热力图:查看变量相关性
numeric_df = df.select_dtypes(include=[np.number])
correlation_matrix = numeric_df.corr()
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', center=0)
plt.title('变量相关性热力图')
plt.tight_layout()
plt.show()
# 小提琴图:查看分布
plt.figure(figsize=(10, 6))
sns.violinplot(x='category', y='amount', data=df, inner='quartile')
plt.title('各类别销售额分布')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# 成对关系图
sns.pairplot(df[['amount', 'quantity', 'user_age', 'order_amount_per_item']])
plt.suptitle('数值变量成对关系', y=1.02)
plt.show()
交互式可视化:plotly
有时候静态图不够用,特别是需要展示给用户看的时候,交互式图表能带来更好的体验。
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# 交互式散点图
fig = px.scatter(df, x='user_age', y='amount', color='category',
size='quantity', hover_data=['order_date', 'city'],
title='用户年龄与销售额关系')
fig.update_layout(template='plotly_white')
fig.show()
# 交互式时间序列图
fig = px.line(df, x='order_date', y='amount', color='city',
title='各城市销售额时间趋势',
markers=True)
fig.update_layout(template='plotly_white')
fig.show()
# 组合图表:柱状图+折线图
fig = make_subplots(specs=[[{"secondary_y": True}]])
# 柱状图:月度订单量
monthly_orders = df.groupby(df['order_date'].dt.month)['order_id'].count()
fig.add_trace(
go.Bar(x=monthly_orders.index, y=monthly_orders.values, name='订单量'),
secondary_y=False
)
# 折线图:月度销售额
monthly_sales = df.groupby(df['order_date'].dt.month)['amount'].sum()
fig.add_trace(
go.Scatter(x=monthly_sales.index, y=monthly_sales.values, name='销售额', mode='lines+markers'),
secondary_y=True
)
fig.update_layout(title='月度订单量与销售额')
fig.update_yaxes(title_text="订单量", secondary_y=False)
fig.update_yaxes(title_text="销售额", secondary_y=True)
fig.show()
地理可视化
如果你的数据包含地理位置信息,地理可视化能带来很直观的效果。
import plotly.express as px
# 城市销售分布地图
city_sales = df.groupby('city')['amount'].sum().reset_index()
fig = px.choropleth(city_sales, locations='city', color='amount',
locationmode='USA cities', scope="china",
title='各城市销售额分布')
fig.show()
# 如果数据有经纬度
fig = px.scatter_mapbox(df, lat='latitude', lon='longitude',
color='amount', size='quantity',
hover_name='city', zoom=3,
mapbox_style='open-street-map')
fig.show()
完整实战:从原始数据到可视化报告
让我用一个完整的例子,带你走一遍从原始数据到可视化报告的全过程。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import warnings
warnings.filterwarnings('ignore')
# 模拟一份电商数据
np.random.seed(42)
n_samples = 10000
data = {
'order_id': range(1, n_samples + 1),
'user_id': np.random.randint(1000, 2000, n_samples),
'city': np.random.choice(['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '西安'], n_samples),
'category': np.random.choice(['电子产品', '服装', '食品', '家居', '美妆', '运动'], n_samples),
'amount': np.random.exponential(500, n_samples).round(2),
'quantity': np.random.randint(1, 10, n_samples),
'user_age': np.random.normal(35, 12, n_samples).astype(int),
'order_date': pd.date_range('2023-01-01', periods=n_samples, freq='H'),
'payment_method': np.random.choice(['支付宝', '微信支付', '银行卡', '货到付款'], n_samples)
}
# 故意制造一些问题
df = pd.DataFrame(data)
df.loc[np.random.choice(n_samples, 200, replace=False), 'amount'] = np.nan
df.loc[np.random.choice(n_samples, 100, replace=False), 'user_age'] = np.nan
df.loc[np.random.choice(n_samples, 50, replace=False), 'city'] = ''
# ========== 第一步:数据清洗 ==========
print("=== 数据清洗 ===")
print(f"原始数据: {df.shape}")
# 删除空城市
df = df[df['city'] != '']
print(f"删除空城市后: {df.shape}")
# 填充缺失值
df['amount'] = df['amount'].fillna(df['amount'].median())
df['user_age'] = df['user_age'].fillna(df['user_age'].median())
print(f"填充缺失值后: {df.shape}")
# 处理异常值
Q1 = df['amount'].quantile(0.01)
Q3 = df['amount'].quantile(0.99)
df = df[(df['amount'] >= Q1) & (df['amount'] <= Q3)]
print(f"处理异常值后: {df.shape}")
# ========== 第二步:特征工程 ==========
print("\n=== 特征工程 ===")
df['order_date'] = pd.to_datetime(df['order_date'])
df['month'] = df['order_date'].dt.month
df['year_month'] = df['order_date'].dt.strftime('%Y-%m')
df['day_of_week'] = df['order_date'].dt.day_name()
df['amount_per_item'] = df['amount'] / df['quantity']
df['is_weekend'] = df['order_date'].dt.weekday >= 5
print(f"最终数据维度: {df.shape}")
print(f"特征列: {[col for col in df.columns]}")
# ========== 第三步:数据探索 ==========
print("\n=== 数据探索 ===")
print(f"\n各城市订单分布:")
print(df['city'].value_counts())
print(f"\n各类别销售额:")
print(df.groupby('category')['amount'].sum().sort_values(ascending=False))
print(f"\n月度销售趋势:")
monthly = df.groupby('year_month')['amount'].sum()
print(monthly)
# ========== 第四步:可视化分析 ==========
print("\n=== 可视化分析 ===")
# 创建仪表板
fig = make_subplots(
rows=2, cols=2,
subplot_titles=('月度销售额趋势', '各类别销售占比', '城市销售排名', '支付渠道分布'),
specs=[[{"type": "scatter"}, {"type": "pie"}],
[{"type": "bar"}, {"type": "pie"}]]
)
# 月度销售额
fig.add_trace(
go.Scatter(x=monthly.index, y=monthly.values, mode='lines+markers',
name='月度销售额', line=dict(color='#1f77b4', width=2)),
row=1, col=1
)
# 类别占比
category_sales = df.groupby('category')['amount'].sum()
fig.add_trace(
go.Pie(labels=category_sales.index, values=category_sales.values,
name="类别占比", hole=0.3, marker=dict(colors=sns.color_palette("husl", 6))),
row=1, col=2
)
# 城市销售
city_sales = df.groupby('city')['amount'].sum().sort_values(ascending=True)
fig.add_trace(
go.Bar(x=city_sales.values, y=city_sales.index, orientation='h',
name="城市销售", marker_color='#ff7f0e'),
row=2, col=1
)
# 支付渠道
payment_dist = df['payment_method'].value_counts()
fig.add_trace(
go.Pie(labels=payment_dist.index, values=payment_dist.values,
name="支付渠道", marker=dict(colors=sns.color_palette("Set2", 4)),
domain={'x': [0.6, 1], 'y': [0, 0.48]}),
row=2, col=2
)
fig.update_layout(
title_text='电商销售数据分析报告',
height=700,
showlegend=False
)
fig.write_html('sales_dashboard.html')
fig.show()
# 保存数据
df.to_csv('cleaned_sales_data.csv', index=False)
print("\n清洗后的数据已保存为 cleaned_sales_data.csv")
常见问题与最佳实践
在实际工作中,你一定会遇到各种各样的问题。这里分享一些我总结的经验和最佳实践。
问题一:数据来自多个源,如何合并?
# 多表合并
df_orders = pd.read_csv('orders.csv')
df_users = pd.read_csv('users.csv')
df_products = pd.read_csv('products.csv')
# 内连接
df_merged = pd.merge(df_orders, df_users, on='user_id', how='inner')
df_merged = pd.merge(df_merged, df_products, on='product_id', how='inner')
# 左连接保留所有订单
df_merged = pd.merge(df_orders, df_users, on='user_id', how='left')
问题二:大数据量如何高效处理?
# 指定数据类型减少内存
df = pd.read_csv('big_data.csv', dtype={
'user_id': 'int32',
'category': 'category',
'amount': 'float32'
})
# 分块读取
chunk_size = 100000
for chunk in pd.read_csv('big_data.csv', chunksize=chunk_size):
# 处理每个数据块
process(chunk)
问题三:如何处理日期时间数据?
# 丰富的日期操作
df['date'] = pd.to_datetime(df['date'])
# 提取日期组件
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['hour'] = df['date'].dt.hour
df['weekofyear'] = df['date'].dt.isocalendar().week
df['quarter'] = df['date'].dt.quarter
df['is_month_start'] = df['date'].dt.is_month_start
df['is_month_end'] = df['date'].dt.is_month_end
df['is_quarter_start'] = df['date'].dt.is_quarter_start
df['is_quarter_end'] = df['date'].dt.is_quarter_end
# 日期偏移
df['next_month'] = df['date'] + pd.DateOffset(months=1)
df['last_week'] = df['date'] - pd.Timedelta(weeks=1)
df['tomorrow'] = df['date'] + pd.Timedelta(days=1)
问题四:如何处理文本数据?
import re
# 文本清洗
def clean_text(text):
if pd.isna(text):
return ''
text = str(text).lower()
text = re.sub(r'http\S+|www\S+', '', text) # 移除URL
text = re.sub(r'[@#\$%&\*\(\)\[\]\{\}:;!\.,?\'\"\\\/|<>—\+\-=_~`]+', ' ', text)
text = re.sub(r'\s+', ' ', text).strip()
return text
df['comment'] = df['comment'].apply(clean_text)
# 文本特征提取
df['comment_length'] = df['comment'].apply(len)
df['word_count'] = df['comment'].apply(lambda x: len(x.split()))
总结
数据分析是一门需要实践的艺术。看完了这些代码和例子,你可能会觉得掌握了,但真正上手的时候还是会遇到各种各样的问题。别担心,这是正常的。
我建议你在实际工作中,找一个你感兴趣的数据集,从头到尾走一遍整个流程。从读取数据、清洗数据、探索数据到可视化展示,每一个环节都亲手操作一遍。遇到报错不要怕,仔细看看错误信息,通常答案就在里面。
记住,好的数据分析不是一蹴而就的,而是反复迭代的过程。先做一个简单的版本,然后再逐步完善。就像写作文一样,先打好草稿,再慢慢润色。
希望这篇教程能帮到你,如果在实际操作中遇到问题,随时可以来交流讨论。数据分析这条路,一个人走可能有点孤单,但一起交流分享,会走得更快更远。
