Python 数据分析
序幕:建立数据
1. 表格数据的导入
01-从 Excel 表格导入
df = pd.read_excel('文件路径.xlsx')
df.head(10)
02-从 csv 文件导入
2. 自行建立表格
# 1. 建立数据
data = {
'Language Code': [
'af', 'sq', 'am', 'ar'
],
'Language Full Name': [
'Afrikaans', 'Albanian', 'Amharic', 'Arabic'
],
'Language Chinese Name': [
'南非荷兰语', '阿尔巴尼亚语', '阿姆哈拉语', '阿拉伯语'
]
}
# 2. 创建DataFrame
df = pd.DataFrame(data)
# 3. 显示结果
print(df)
添加一行
添加一列
第一部分:数据预处理
1. 空值问题
df['列名'].isnull().sum()) # 查看 NaN 个数
df['列名'] = df['列名'].fillna(0) # 将 NaN 替换为 0
2. 属性的数据类型转换
df.dtypes # 显示所有列的数据类型
df['列名'].dtypes # 查看某一列的数据类型
df['列名'] = df['列名'].astype(某种数据类型) # 将某一列转化为某种数据类型
- 显示某种数据类型的所有属性
int_column_names = df.select_dtypes(include='int64').columns # 显示 int64 类型的所有属性
print(", ".join(int_column_names))
- 连续型数据类型分段
# 以 df['年龄'] 举例
bins = [2, 4, 6, 8, 10, 12, 14, 16, 18, float('inf')] # 添加一个无限大区间
labels = ['2-4', '4-6', '6-8', '8-10', '10-12', '12-14', '14-16', '16-18', '>18']
# 对年龄数据进行分箱
df['年龄区间'] = pd.cut(df['年龄'], bins=bins, labels=labels, right=False)
# 统计每个区间的数量
counts = df['年龄区间'].value_counts(sort=False)
# 绘制直方图
counts.plot(kind='bar', edgecolor='black')
3. 数据分组
当数据存在多个维度的时候——有许多因素可以对对象进行描述,这个时候就需要根据需求对某一些因素进行归类分组,就需要用到 pd.groupby()
例子1:号码资源分析
- 数据解释:
- 假设某号码资源有三种状态:空闲、在途、冻结
- 现在要统计在某个号码省、号码地市空闲号码的占比
big_area 号码省 号码地市 状态 count(1)
0 s 北京 北京 空闲 1087
1 n 山东 淄博 冻结 164
2 n 山东 滨州 在途 164
3 s 山东 临沂 空闲 27
4 s 山东 潍坊 空闲 187
...
- 步骤
- 使用
groupby
对号码省、号码地市、状态进行分组;
status_counts = df.groupby(['号码省', '号码地市', '状态'])['count(1)'].sum().unstack(fill_value=0)
"""
- 知识点:
df.groupby(by)[列].聚合函数()
- by:分组的依据,可以是某一列、多个列、函数等;
- [列]:要对哪些列进行操作;
- 聚合函数:如 .sum()、.mean()、.count()、.max()、.min() 等。
- 代码解读
- groupby(['号码省', '号码地市', '状态'])
- 按省、市、状态三级分组。
- ['count(1)'].sum()
- 对每个组的 count(1) 求和。
- .unstack(fill_value=0)
- 将“状态”这一列从行索引展开成列索引(类似 Excel 透视表),填充空值为 0。
- 结果:
号码省,号码地市,冻结,占用,在途,空闲,预占
上海,上海,1173,15843,146,4542,29
云南,临沧,4,48,3,109,0
云南,丽江,7,138,62,7,0
云南,保山,10,86,2,27,0
云南,大理,13,105,11,24,0
"""
- 使用
sum
计算状态总和
cal_col = ['冻结', '占用', '在途', '空闲', '预占']
status_counts['总记录数'] = status_counts[cal_col].sum(axis=1)
"""
- 知识点:
添加 cal_col 是比较规范的做法,虽然 pandas 默认只对数值列(numeric columns)求和,非数值列(比如字符串)会被自动忽略
- 结果:
号码省,号码地市,冻结,占用,在途,空闲,预占,总记录数
上海,上海,1173,15843,146,4542,29,21733
云南,临沧,4,48,3,109,0,164
云南,丽江,7,138,62,7,0,214
云南,保山,10,86,2,27,0,125
"""
- 计算空闲占比
- 重置索引并整理列顺序
后续就与分组无关啦,最终结果如下:
号码省,号码地市,冻结,占用,在途,空闲,预占,总记录数,空闲百分比
上海,上海,1173,15843,146,4542,29,21733,20.9%
云南,临沧,4,48,3,109,0,164,66.46%
云南,丽江,7,138,62,7,0,214,3.27%
云南,保山,10,86,2,27,0,125,21.6%
第二部分:数据分析
0. 变量重命名
- 查看某属性有哪些分类
df['列名'].unique() # 查看该列的分类
- 更改分类名
df['性别'] = df['性别'].replace({0: '女', 1: '男'}) # 用性别举例
- 如果是分类变量,推荐使用
df['性别'] = df['性别'].cat.rename_categories({0: '女', 1: '男'})
1. 分类变量和分类变量
"""
拿性别和是否续报举例,其中性别
"""
# 计算分类和数据表
crosstab = pd.crosstab(df['性别'], df['是否续报'])
proportions = crosstab.div(crosstab.sum(axis=1), axis=0) # 按性别分组计算比例
table = tabulate(proportions, headers='keys', tablefmt='grid', showindex=True)
print(table)
# 绘制堆积柱状图
ax = proportions.plot(kind='bar', stacked=True, color=['lightblue', 'orange', 'pink'], edgecolor='black')
# 添加比例标注
for i, (gender, row) in enumerate(proportions.iterrows()): # 遍历每个性别的比例数据
bottom = 0 # 初始化底部位置
for category, value in row.items(): # 遍历每个分类(是否报名)的比例
if value > 0: # 只标注非零比例
plt.text(
i,
bottom + value / 2, # 底部位置 + 当前堆积柱的中点
f'{value:.1%}', # 转换为百分比形式
ha='center', va='center', fontsize=12, color='black'
)
bottom += value # 更新底部位置
# 设置标题和标签
plt.title('“性别”与“是否续报”的关系')
plt.xlabel('性别')
plt.xticks(rotation=0)
plt.ylabel('比例')
plt.legend(title='是否续报', loc='upper right')
plt.tight_layout() # 自动调整布局
plt.show()
# 卡方检验
chi2, p, dof, expected = chi2_contingency(crosstab)
print(f"卡方统计量: {chi2}")
print(f"p值: {p}")
- 两个分类变量
import pandas as pd
import matplotlib.pyplot as plt
from scipy.stats import chi2_contingency
# 定义年龄段分组
bins = [0, 4, 8, 12, 16, float('inf')]
labels = ['<4', '4-8', '8-12', '12-16', '>16']
df['年龄段'] = pd.cut(df['年龄'], bins=bins, labels=labels)
# 创建多维交叉表
crosstab = pd.crosstab([df['年龄段'], df['性别']], df['是否续报'])
proportions = crosstab.div(crosstab.sum(axis=1), axis=0) # 按每个年龄段-性别组计算比例
table = tabulate(proportions, headers='keys', tablefmt='grid', showindex=True)
print(table)
# 绘制堆积柱状图
ax = proportions.unstack(level=1).plot(
kind='bar',
stacked=True,
color=['lightblue', 'orange', 'pink'],
edgecolor='black',
figsize=(12, 6)
)
# 设置标题和标签
plt.title('“年龄段”与“性别”对“是否续报”的影响')
plt.xlabel('年龄段-性别组合')
plt.ylabel('比例')
plt.xticks(rotation=45)
plt.legend(title='是否续报', loc='upper right')
plt.tight_layout()
plt.show()
# 卡方检验
chi2, p, dof, expected = chi2_contingency(crosstab)
print("卡方检验结果:")
print(f"卡方统计量: {chi2:.2f}")
print(f"p值: {p:.4f}")
print(f"自由度: {dof}")
2. 数值变量和分类变量
# 更新分组
bins = [0, 4, 8, 12, 16, float('inf')] # 重新定义年龄段
labels = ['<4', '4-8', '8-12', '12-16', '>16'] # 更新标签
# 年龄分段
df['年龄段'] = pd.cut(df['年龄'], bins=bins, labels=labels)
# 计算分类和数据表
crosstab = pd.crosstab(df['年龄段'], df['是否续报'])
proportions = crosstab.div(crosstab.sum(axis=1), axis=0) # 按年龄段分组计算比例
table = tabulate(proportions, headers='keys', tablefmt='grid', showindex=True)
print(table)
# 绘制堆积柱状图
ax = proportions.plot(kind='bar', stacked=True, color=['lightblue', 'orange', 'pink'], edgecolor='black')
for i, (age_group, row) in enumerate(proportions.iterrows()): # 遍历每个年龄段的比例数据
bottom = 0 # 初始化底部位置
for category, value in row.items(): # 遍历每个分类(是否续报)的比例
if value > 0: # 只标注非零比例
plt.text(
i,
bottom + value / 2, # 底部位置 + 当前堆积柱的中点
f'{value:.1%}', # 转换为百分比形式
ha='center', va='center', fontsize=12, color='black'
)
bottom += value # 更新底部位置
# 设置标题和标签
plt.title('“年龄段”与“是否续报”的关系')
plt.xlabel('年龄段')
plt.xticks(rotation=0)
plt.ylabel('比例')
# 调整图例位置
plt.legend(title='是否续报', loc='upper center', bbox_to_anchor=(0.5, -0.1), ncol=3, frameon=False)
# 自动调整布局
plt.tight_layout()
plt.show()
# 卡方检验
chi2, p, dof, expected = chi2_contingency(crosstab)
print(f"卡方统计量: {chi2}")
print(f"p值: {p}")
3. 时序变量和分类变量
df['是否续报'] = df['是否续报'].map({'是': 1, '否': 0}).astype(float)
# 将分类变量转化为浮点数来计算比率
df['赛考确认日期'] = df['赛考确认时间'].dt.date
# 时间太散
# 对 2024-06-27 15:04:26 这种值,dt.date 会提取出 2024-06-27
trend = df.groupby('赛考确认日期')['是否续报'].mean()
trend.plot(kind='line', marker='o', color='blue')
plt.title('赛考确认时间与续报率的关系')
plt.xlabel('赛考确认时间')
plt.xticks(rotation=60)
plt.ylabel('续报率')
plt.grid(True)
plt.show()
df['是否续报'] = df['是否续报'].map({'是': 1, '否': 0}).astype(float)
df['续报结束日期'] = pd.to_datetime(df['续报结束日期'], errors='coerce')
# 筛选特定日期范围内的数据
start_date = pd.to_datetime('2024-09-15')
end_date = pd.to_datetime('2024-12-08')
df_filtered = df[(df['续报结束日期'] >= start_date) & (df['续报结束日期'] <= end_date)]
trend = df_filtered.groupby('续报结束日期')['是否续报'].mean()
trend.plot(kind='line', marker='o', color='blue')
plt.title('续报结束日期与续报率的关系')
plt.xlabel('续报结束日期')
plt.xticks(rotation=60)
plt.ylabel('续报率')
plt.grid(True)
plt.show()
番外:
1. 解决Mac中 matplotlib 绘图中文乱码问题
- 下载 SimHei 中文字体
- 保存到 Mac 应用字体册中(字体册 - 文件 - 将字体添加到“当前用户”)
- 找到缓存的位置
- 删除缓存
- 重新载入项目,输入代码
删除 matplotlib 的缓存
import matplotlib as mpl
print(mpl.get_cachedir())
cd 缓存地址
ls
rm *
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False