掌握Python数据分析进阶课程从数据清洗到可视化实战
说实话,数据分析这条路,很多人刚开始都是懵的。你看着那一堆乱糟糟的数据,脑子里只有一句话:这玩意儿到底怎么分析?别慌,今天咱们就从头到尾把这事儿掰开揉碎了讲清楚。
数据清洗,才是你离高手最近的一步
很多人一上来就想着”我要做可视化”“我要搞预测模型”,结果一看数据,直接傻眼。
为什么?因为数据太脏了。
真实世界的数据,从来都不是干净的。它有缺失值、有重复项、有格式乱七八糟的日期、有让人摸不着头脑的异常值……你如果不先把这些处理掉,后面做的一切都像是建在沙地上的房子,随时会塌。
缺失值怎么处理?
我给你看个真实的例子。假设你在分析一份员工数据,里面有个字段叫”工资”,但有些人的工资是空的:
import pandas as pd
import numpy as np
# 模拟一份员工数据
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '钱七'],
'年龄': [28, 35, np.nan, 42, 31],
'工资': [8000, np.nan, 12000, np.nan, 9500],
'部门': ['技术部', '市场部', '技术部', '财务部', np.nan]
})
print("原始数据:")
print(df)
print("\n缺失值统计:")
print(df.isnull().sum())
输出结果:
原始数据:
姓名 年龄 工资 部门
0 张三 28.0 8000.0 技术部
1 李四 35.0 NaN 市场部
2 王五 NaN 12000.0 技术部
3 赵六 42.0 NaN 财务部
4 钱七 31.0 9500.0 NaN
缺失值统计:
姓名 0
年龄 1
工资 2
部门 1
dtype: int64
你看,这就是现实中的数据。面对缺失值,一般有几种处理方式:
第一种,直接删除。 适用于缺失比例很小的情况:
# 删除含缺失值的行
df_clean = df.dropna()
print("删除缺失值后的数据:")
print(df_clean)
第二种,填充缺失值。 这是最常用的方法,但填充什么值有讲究:
# 用均值填充年龄
df['年龄'] = df['年龄'].fillna(df['年龄'].mean())
# 用中位数填充工资(避免极端值影响)
df['工资'] = df['工资'].fillna(df['工资'].median())
# 用众数填充部门(类别数据用众数最合理)
df['部门'] = df['部门'].fillna(df['部门'].mode()[0])
print("填充后的数据:")
print(df)
第三种,用业务逻辑填充。 比如工资这个字段,如果缺失的是”技术部”的员工,你可以用技术部的平均工资来填充:
# 按部门填充工资
df['工资'] = df.groupby('部门')['工资'].transform(
lambda x: x.fillna(x.mean())
)
print("按部门填充工资后:")
print(df)
重复值怎么办?
重复数据会让你统计结果完全错误。比如你算平均工资,重复一行数据就会被多算一次:
# 检查重复值
print("重复行数:", df.duplicated().sum())
# 删除重复行
df = df.drop_duplicates()
数据类型转换
很多时候,数据读进来的类型不对。比如日期读成了字符串,数字读成了对象类型:
# 查看当前数据类型
print(df.dtypes)
# 转换数据类型
df['年龄'] = df['年龄'].astype(int)
df['工资'] = df['工资'].astype(float)
df['入职日期'] = pd.to_datetime(df['入职日期'], errors='coerce')
异常值检测和处理
异常值是最让人头疼的。一个数据点错误,可能会把整个分析结论带偏。
import matplotlib.pyplot as plt
# 用箱线图检测异常值
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 年龄箱线图
axes[0].boxplot(df['年龄'].dropna())
axes[0].set_title('年龄箱线图')
axes[0].set_xlabel('年龄')
# 工资箱线图
axes[1].boxplot(df['工资'].dropna())
axes[1].set_title('工资箱线图')
axes[1].set_xlabel('工资')
plt.tight_layout()
plt.savefig('异常值检测.png', dpi=150)
plt.show()
箱线图里那些小圆圈就是异常值。处理方式有几种:
# 方法一:用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} ~ {upper_bound}")
# 截断异常值,而不是直接删除(保留数据量)
df['工资'] = df['工资'].clip(lower=lower_bound, upper=upper_bound)
数据探索,让你真正理解数据
清洗完之后,别急着做可视化。你得先了解你的数据长什么样。
# 数据基本信息
print("数据形状:", df.shape)
print("\n数据概览:")
print(df.describe())
print("\n数据信息:")
df.info()
describe() 会给你数字列的统计摘要,info() 会告诉你每列的数据类型和缺失情况。这两个是你最常用的工具。
单变量分析
先看看每个变量自己长什么样:
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
# 年龄分布
axes[0, 0].hist(df['年龄'], bins=15, color='steelblue', edgecolor='white')
axes[0, 0].set_title('年龄分布')
axes[0, 0].set_xlabel('年龄')
# 工资分布
axes[0, 1].hist(df['工资'], bins=15, color='coral', edgecolor='white')
axes[0, 1].set_title('工资分布')
axes[0, 1].set_xlabel('工资')
# 部门分布
axes[1, 0].bar(df['部门'].value_counts().index,
df['部门'].value_counts().values,
color='mediumseagreen')
axes[1, 0].set_title('部门分布')
axes[1, 0].set_xlabel('部门')
axes[1, 0].set_ylabel('人数')
# 年龄和工资的关系
axes[1, 1].scatter(df['年龄'], df['工资'], alpha=0.6, color='rebeccapurple')
axes[1, 1].set_title('年龄与工资关系')
axes[1, 1].set_xlabel('年龄')
axes[1, 1].set_ylabel('工资')
plt.tight_layout()
plt.savefig('单变量分析.png', dpi=150)
plt.show()
这些图看起来简单,但信息量很大。比如年龄和工资的关系图,你能一眼看出两者是不是正相关,有没有异常点。
多变量分析
单变量分析看完之后,要看看变量之间的关系:
# 相关性热力图
numeric_df = df.select_dtypes(include=[np.number])
correlation = numeric_df.corr()
plt.figure(figsize=(8, 6))
sns.heatmap(correlation, annot=True, cmap='coolwarm', center=0,
fmt='.2f', square=True)
plt.title('变量相关性热力图')
plt.tight_layout()
plt.savefig('相关性热力图.png', dpi=150)
plt.show()
热力图能帮你快速找出哪些变量之间有强相关性。比如年龄和工资如果相关性很高,说明工作经验可能是影响工资的重要因素。
数据可视化,让数据会说话
数据清洗好了,探索也做了,现在该让数据”说话”了。可视化不是画几张漂亮的图就完了,你的每一张图都要能传递信息。
Matplotlib:基础但强大
# 创建一份更丰富的工作数据
np.random.seed(42)
dates = pd.date_range('2023-01-01', periods=365, freq='D')
df_work = pd.DataFrame({
'日期': dates,
'日销售额': np.random.randint(1000, 5000, 365),
'日订单数': np.random.randint(10, 100, 365),
'客户数': np.random.randint(50, 500, 365),
'渠道': np.random.choice(['线上', '线下', '电话'], 365, p=[0.5, 0.3, 0.2])
})
# 销售额趋势图
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 每日销售额趋势
axes[0, 0].plot(df_work['日期'], df_work['日销售额'],
color='steelblue', linewidth=0.8, alpha=0.7)
axes[0, 0].set_title('每日销售额趋势', fontsize=12)
axes[0, 0].set_xlabel('日期')
axes[0, 0].set_ylabel('销售额(元)')
axes[0, 0].grid(True, alpha=0.3)
# 按渠道的销售额分布
channel_sales = df_work.groupby('渠道')['日销售额'].mean().sort_values(ascending=False)
axes[0, 1].bar(channel_sales.index, channel_sales.values,
color=['#e74c3c', '#3498db', '#2ecc71'])
axes[0, 1].set_title('各渠道平均日销售额', fontsize=12)
axes[0, 1].set_xlabel('渠道')
axes[0, 1].set_ylabel('平均日销售额(元)')
# 日订单数分布
axes[1, 0].hist(df_work['日订单数'], bins=20, color='#9b59b6',
edgecolor='white', alpha=0.7)
axes[1, 0].set_title('日订单数分布', fontsize=12)
axes[1, 0].set_xlabel('订单数')
axes[1, 0].set_ylabel('频次')
# 销售额与订单数的关系
axes[1, 1].scatter(df_work['日订单数'], df_work['日销售额'],
alpha=0.5, color='#1abc9c', s=20)
axes[1, 1].set_title('日订单数与销售额关系', fontsize=12)
axes[1, 1].set_xlabel('日订单数')
axes[1, 1].set_ylabel('日销售额(元)')
plt.tight_layout()
plt.savefig('工作数据分析.png', dpi=150, bbox_inches='tight')
plt.show()
Seaborn:让统计可视化更简单
# 使用Seaborn做更专业的统计图
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 销售额的箱线图(按渠道分)
sns.boxplot(data=df_work, x='渠道', y='日销售额', ax=axes[0, 0],
palette='Set2')
axes[0, 0].set_title('各渠道销售额分布(箱线图)')
# 日销售额的核密度估计
sns.kdeplot(data=df_work, x='日销售额', ax=axes[0, 1],
fill=True, color='coral', alpha=0.6)
axes[0, 1].set_title('日销售额分布(核密度估计)')
# 销售额与订单数的联合分布
sns.jointplot(data=df_work, x='日订单数', y='日销售额',
kind='scatter', color='mediumpurple', alpha=0.5)
axes[1, 0].set_title('销售额与订单数联合分布')
# 按月份的销售额趋势
df_work['月份'] = df_work['日期'].dt.month
monthly_sales = df_work.groupby('月份')['日销售额'].mean()
sns.lineplot(data=monthly_sales, marker='o', ax=axes[1, 1],
color='darkorange')
axes[1, 1].set_title('月度平均日销售额趋势')
axes[1, 1].set_xlabel('月份')
axes[1, 1].set_ylabel('平均日销售额(元)')
plt.tight_layout()
plt.savefig('seaborn分析图.png', dpi=150, bbox_inches='tight')
plt.show()
Plotly:交互式可视化
静态图看完就完了,但交互式图表能让你的受众自己去探索数据:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# 交互式销售额趋势图
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_work['日期'],
y=df_work['日销售额'],
mode='lines',
name='日销售额',
line=dict(color='steelblue', width=1.5)
))
fig.add_trace(go.Scatter(
x=df_work['日期'],
y=df_work['日销售额'].rolling(window=7).mean(),
mode='lines',
name='7日移动平均',
line=dict(color='crimson', width=2)
))
fig.update_layout(
title='日销售额趋势(含7日移动平均)',
xaxis_title='日期',
yaxis_title='销售额(元)',
hovermode='x unified',
template='plotly_white'
)
fig.write_html('销售额趋势图.html')
fig.show()
实战案例:电商销售数据分析
光说不练假把式。我给你一个完整的实战案例,从数据读取到最终报告,一步步走下来。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings('ignore')
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# ========================================
# 第一步:读取数据
# ========================================
# 假设你有一份电商销售数据
# 实际工作中,数据可能来自CSV、Excel、数据库等
df = pd.read_csv('ecommerce_sales.csv')
print("=" * 50)
print("数据基本信息")
print("=" * 50)
print(f"数据行数:{df.shape[0]}")
print(f"数据列数:{df.shape[1]}")
print(f"\n列名:{list(df.columns)}")
print(f"\n前5行数据:")
print(df.head())
print(f"\n数据类型:")
print(df.dtypes)
print(f"\n缺失值统计:")
print(df.isnull().sum())
print(f"\n数值列统计:")
print(df.describe())
假设数据有以下字段:订单ID、订单日期、客户ID、产品类别、产品单价、数量、销售额、地区、支付方式、配送状态。
# ========================================
# 第二步:数据清洗
# ========================================
print("\n" + "=" * 50)
print("开始数据清洗...")
print("=" * 50)
# 1. 删除重复订单
before_count = len(df)
df = df.drop_duplicates(subset=['订单ID'])
after_count = len(df)
print(f"删除重复订单:{before_count - after_count} 条")
# 2. 处理缺失值
print(f"\n清洗前缺失值:{df.isnull().sum().sum()}")
# 销售额为空的行,尝试用单价*数量补全
df['销售额'] = df['销售额'].fillna(df['产品单价'] * df['数量'])
# 地区缺失的用众数填充
df['地区'] = df['地区'].fillna(df['地区'].mode()[0])
# 客户ID缺失的行删除(没有客户信息的数据价值低)
df = df.dropna(subset=['客户ID'])
print(f"清洗后缺失值:{df.isnull().sum().sum()}")
# 3. 数据类型转换
df['订单日期'] = pd.to_datetime(df['订单日期'])
df['数量'] = df['数量'].astype(int)
df['产品单价'] = df['产品单价'].astype(float)
df['销售额'] = df['销售额'].astype(float)
# 4. 异常值处理
# 用IQR法则处理销售额异常值
Q1 = df['销售额'].quantile(0.01) # 用1%和99%分位数,避免极端值
Q3 = df['销售额'].quantile(0.99)
lower_bound = Q1
upper_bound = Q3
# 记录被截断的数据
outlier_count = ((df['销售额'] < lower_bound) | (df['销售额'] > upper_bound)).sum()
print(f"销售额异常值(将被截断):{outlier_count} 条")
df['销售额'] = df['销售额'].clip(lower=lower_bound, upper=upper_bound)
# 5. 新增分析字段
df['订单年份'] = df['订单日期'].dt.year
df['订单月份'] = df['订单日期'].dt.month
df['订单季度'] = df['订单日期'].dt.quarter
df['星期'] = df['订单日期'].dt.day_name()
df['客单价'] = df['销售额'] / df['数量']
# ========================================
# 第三步:探索性分析
# ========================================
print("\n" + "=" * 50)
print("开始探索性分析...")
print("=" * 50)
# 1. 基础统计
print(f"\n总订单数:{len(df):,}")
print(f"总销售额:{df['销售额'].sum():,.2f} 元")
print(f"平均订单金额:{df['销售额'].mean():.2f} 元")
print(f"中位数订单金额:{df['销售额'].median():.2f} 元")
print(f"客户总数:{df['客户ID'].nunique():,}")
print(f"产品类别数:{df['产品类别'].nunique()}")
print(f"地区数:{df['地区'].nunique()}")
# 2. 按时间的销售趋势
monthly_sales = df.groupby('订单月份')['销售额'].sum()
print(f"\n月度销售趋势:")
print(monthly_sales)
# 3. 按产品类别的销售分布
category_sales = df.groupby('产品类别')['销售额'].sum().sort_values(ascending=False)
print(f"\n产品类别销售排名:")
print(category_sales)
# 4. 按地区的销售分布
region_sales = df.groupby('地区')['销售额'].sum().sort_values(ascending=False)
print(f"\n地区销售排名:")
print(region_sales)
# ========================================
# 第四步:数据可视化
# ========================================
# 创建一个综合仪表盘
fig = make_subplots(
rows=3, cols=2,
subplot_titles=(
'月度销售趋势', '产品类别销售分布',
'地区销售分布', '日星期销售趋势',
'订单金额分布', '支付方式占比'
),
specs=[
[{"type": "scatter"}, {"type": "pie"}],
[{"type": "bar"}, {"type": "bar"}],
[{"type": "histogram"}, {"type": "pie"}]
],
vertical_spacing=0.12,
horizontal_spacing=0.1
)
# 1. 月度销售趋势(折线图)
fig.add_trace(
go.Scatter(
x=monthly_sales.index,
y=monthly_sales.values,
mode='lines+markers',
name='月度销售额',
line=dict(color='steelblue', width=2),
marker=dict(size=8)
),
row=1, col=1
)
# 2. 产品类别销售分布(饼图)
fig.add_trace(
go.Pie(
labels=category_sales.index,
values=category_sales.values,
name="产品类别",
hole=0.4,
marker=dict(colors=['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c']),
textinfo='label+percent'
),
row=1, col=2
)
# 3. 地区销售分布(水平条形图)
fig.add_trace(
go.Bar(
y=region_sales.index,
x=region_sales.values,
orientation='h',
name='地区销售',
marker=dict(color='coral')
),
row=2, col=1
)
# 4. 星期销售趋势
weekday_sales = df.groupby('星期')['销售额'].mean()
weekday_order = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
weekday_sales = weekday_sales.reindex(weekday_order)
fig.add_trace(
go.Bar(
x=weekday_sales.index,
y=weekday_sales.values,
name='日均销售额',
marker=dict(color='mediumpurple')
),
row=2, col=2
)
# 5. 订单金额分布(直方图)
fig.add_trace(
go.Histogram(
x=df['销售额'],
name='订单金额',
nbinsx=30,
marker=dict(color='teal', line=dict(color='white', width=0.5)),
showlegend=False
),
row=3, col=1
)
# 6. 支付方式占比
payment_ratio = df['支付方式'].value_counts()
fig.add_trace(
go.Pie(
labels=payment_ratio.index,
values=payment_ratio.values,
name="支付方式",
hole=0.4,
marker=dict(colors=['#e67e22', '#2980b9', '#27ae60', '#8e44ad']),
textinfo='label+percent',
showlegend=False
),
row=3, col=2
)
fig.update_layout(
title_text='电商销售数据综合分析仪表盘',
title_x=0.5,
title_font_size=20,
showlegend=False,
height=900,
template='plotly_white'
)
fig.write_html('电商销售分析仪表盘.html')
fig.show()
# ========================================
# 第五步:深入分析
# ========================================
# 1. 客户价值分析(RFM模型简化版)
customer_analysis = df.groupby('客户ID').agg({
'订单日期': 'max', # 最近一次购买
'订单ID': 'count', # 购买次数
'销售额': 'sum' # 总消费金额
}).rename(columns={
'订单日期': '最近购买日期',
'订单ID': '购买次数',
'销售额': '总消费金额'
})
# 计算客户生命周期天数
customer_analysis['购买天数'] = (
customer_analysis['最近购买日期'].max() -
customer_analysis['最近购买日期']
).dt.days
# 计算客户价值得分
customer_analysis['价值得分'] = (
customer_analysis['总消费金额'] / customer_analysis['总消费金额'].max() * 0.5 +
customer_analysis['购买次数'] / customer_analysis['购买次数'].max() * 0.3 +
(customer_analysis['购买天数'].max() - customer_analysis['购买天数']) /
customer_analysis['购买天数'].max() * 0.2
)
# 客户分层
def classify_customer(score):
if score >= 0.7:
return '高价值客户'
elif score >= 0.4:
return '潜力客户'
elif score >= 0.2:
return '一般客户'
else:
return '低价值客户'
customer_analysis['客户类型'] = customer_analysis['价值得分'].apply(classify_customer)
print("\n客户分层结果:")
print(customer_analysis['客户类型'].value_counts())
# 2. 产品关联分析
product_basket = df.groupby('产品类别').agg({
'订单ID': 'nunique',
'销售额': 'sum',
'数量': 'sum'
}).rename(columns={
'订单ID': '订单数',
'销售额': '总销售额',
'数量': '总销量'
})
product_basket = product_basket.sort_values('总销售额', ascending=False)
print("\n产品类别经营分析:")
print(product_basket)
几个实战中的小窍门
1. 数据读取时直接指定类型,能省很多事
# bad: 读进来全是object,后面还要转换
df = pd.read_csv('data.csv')
# good: 直接指定类型
df = pd.read_csv('data.csv',
parse_dates=['订单日期'],
dtype={'客户ID': str, '地区': 'category'})
2. 用管道式写法,代码更清晰
# 把一系列清洗操作串起来
df_clean = (df
.drop_duplicates(subset=['订单ID'])
.dropna(subset=['客户ID'])
.assign(
订单日期=lambda x: pd.to_datetime(x['订单日期']),
销售额=lambda x: x['产品单价'] * x['数量']
)
.query("销售额 > 0 and 销售额 < 50000")
)
3. 可视化时,记住”少即是多”
不要一张图堆太多信息。你的观众需要一眼就能看懂你在说什么。如果一张图放不下,就拆成两张。
# 好的做法:清晰的标签、合适的颜色、不过度的装饰
fig, ax = plt.subplots(figsize=(10, 5))
ax.barh(product_basket.index, product_basket['总销售额'], color='steelblue')
ax.set_xlabel('总销售额(元)')
ax.set_title('各产品类别销售额对比')
ax.invert_yaxis() # 让最高的在上方
plt.tight_layout()
plt.savefig('产品类别分析.png', dpi=150)
4. 保存你的分析结果
分析完不能只停留在屏幕上,要把结果存下来:
# 保存清洗后的数据
df_clean.to_csv('cleaned_data.csv', index=False, encoding='utf-8-sig')
# 保存关键指标
summary = pd.DataFrame({
'指标': ['总订单数', '总销售额', '平均客单价', '客户总数'],
'数值': [len(df), df['销售额'].sum(),
df['销售额'].mean(), df['客户ID'].nunique()]
})
summary.to_csv('analysis_summary.csv', index=False, encoding='utf-8-sig')
# 保存可视化图表
fig.savefig('final_report.png', dpi=200, bbox_inches='tight')
最后说几句
数据分析这门手艺,说白了就是”脏活累活”占大头。你可能花80%的时间在清洗数据、理解数据,只有20%的时间在做模型和可视化。但这20%的输出,决定了你的工作有没有价值。
我见过太多人,一上来就搞复杂模型,结果数据都没洗干净,模型再厉害也是垃圾进垃圾出。反过来,如果你能把数据摸得很透,哪怕只是做一些简单的统计分析,也能给出很有洞察力的结论。
所以,别嫌数据清洗枯燥。每一次你清理缺失值、处理异常值、统一格式,你都在让数据变得更诚实、更有价值。可视化也不是为了好看,而是为了让那些隐藏在数字背后的故事能被看见。
把这些基本功练扎实了,你再去学什么机器学习、深度学习,都会发现那些东西其实不难。因为数据分析的核心能力——理解数据、提出问题、验证假设——是相通的。
现在,打开你的Python,找一份数据,开始你的清洗之旅吧。数据不会骗人,关键是你有没有耐心去了解它。
