项目概要

项目名称

基于中国铁路12306公布的火车信息等数据进行数据分析与数据可视化

项目背景

随着中国经济的持续发展和城市化进程的加速,人口流动日益频繁,铁路运输作为中国最重要的公共交通方式之一,承载了巨大的客运量。本项目旨在利用中国铁路12306公布的火车票价格等公开数据,通过数据分析与可视化技术,探索票价趋势、出行模式,提供直观的数据洞察,辅助决策优化和服务提升。

分析范围

本项目主要分析中国铁路12306公布的较为繁忙的铁路线路的火车车次、火车类型、火车出发时刻/到站时刻、火车始发站/终点站、火车票价等进行数学分析和数据可视化。

实现思路

[!NOTE]

为确保项目实现过程与课程教学内容紧密对应,本项目将依据课程中讲授的知识点逐步实现相应功能。尽管实现方法可能非最高效途径,但旨在强化理论与实践的结合,确保学习效果。

编写开发文档

一个好的项目避免不了需要一个好的开发文档,为了实现此项目,特编写了这篇开发文档,因为了保持和课程教学内容紧密对应,本篇文档将采用MarkDown编写。

数据采集

  1. 本次选取了piao.gaotie.cn网站进行火车数据的数据采集,并加入了代理IP进行高效爬取,谨防出现IP被封禁等情况。
  2. 为了更好的实现数据采集的功能,我将数据采集(GetData)代理(Proxy)制作成了两个软件包,供主函数调用。
  3. 为了更高效的使用IP代理,减少数据采集带来的麻烦。我将代理IP的软件包制作成了一个Class(类)对象,进行代理调度让采集更高效,以更好的实现数据采集。

通过上述步骤,将采集好的数据通过Python中的List(列表)特有的数据类型返回给主函数进行调用。

[!WARNING]

本项目侧重点为数据分析与可视化,故数据采集的具体实现方法与过程将不再过多讲述,详情可查看源代码。

数据写入与保存

将数据采集返回的数据保存到运行目录下的/data文件夹下(在保存前,应先判断此路径是否存在,如不存在应先创建),将为每一条火车线路单独创建一个csv文件,实现数据的分别保存。

    # 获取火车数据并写入csv文件,准备数据处理
    # 注意:如是火车,则动车组二等座的价格为硬座价格。
    # TODO 数据获取与写入
    # START------------------------------------------------
    data, city = GetData_Package.get_data() # 获取数据的函数
    for index, item in enumerate(data): #循环取值
        city_from = city[index]["from"][0] #获取火车的出发地
        city_to = city[index]["to"][0] #获取火车的目的地
        with open(r"data/" + city_from + "-" + city_to + ".csv", "w", newline="", encoding="gbk") as files: #创建文件对象
            writer = csv.writer(files) # 创建csv对象
            writer.writerow(
                ['车次', '空白值1', '类型', '出发站', '空白值2', '到达站', '出发时间', '到达时间', '运行时长', '动车组票价', '火车组票价', '空白值3']) # 在数据爬取中,有空白列,在后续处理
            writer.writerows(item) # 写入数据
    # END------------------------------------------------

[!CAUTION]

至此,数据采集与文件写入已完成,下面将开始进行数据分析。

数据分析

前提条件:读入csv文件

进行数据分析的前提需要读入csv文件,在csv文件中进行分析。

因本项目中,将每一条火车线路都单独创建了一个csv文件,所以在读入时,我采用了循环读入文件。

    data, city = GetData_Package.get_data() # 获取采集后的数据
    for index, item in enumerate(data):
        city_from = city[index]["from"][0] #获取火车的出发地
        city_to = city[index]["to"][0] #获取火车的目的地
        file_path = f"data/{city_from}-{city_to}.csv"
        # 读取CSV文件到 DataFrame
        df = pd.read_csv(file_path, encoding='gbk', na_values=' - ')
        # 后续数据分析的代码都在此循环内依次进行

数据分析_第一步:空白列删除(缺失值处理)

因数据采集后的数据有多余无意义的数据,为了保证数据分析的可靠性,我们需要先将多余无意义的数据进行删除。

在本项目中,数据采集之后生成了多余的空白列,所以在此处我们先进行空白列的删除。

我们定义了一个List(columns_to_keep),将需要保留的列赋值给了它。运用pandas库中的函数删除不存在上述列表中的列,达到空白列的删除。

# 处理数据第一步:删除空白列
# 背景:在数据爬取中,有空白列,我们在这里删除
# TODO 数据处理:删除空白列
# START------------------------------------------------
columns_to_keep = ['车次', '类型', '出发站', '到达站', '出发时间', '到达时间', '运行时长', '动车组票价',
                   '火车组票价']  # 定义需要保留的列
df = df[columns_to_keep] # 删除不在 columns_to_keep 列表中的所有列
# END------------------------------------------------

数据分析_第二步:火车价格的数据处理(分割数据)

在本项目中,数据采集后的数据将火车的火车价格都放在一个单元格中(例如在火车中,爬取后的数据将卧铺的价格放在了一个单元格内,但原本火车价格应有卧铺上铺、中铺、下铺三种席位,这三种席位,每种席位有单独的价格,故我们应该将这三个价格分别放置在三个不同单元格中),需要对价格进行分割,否则会造成后续进行票价分析中出现问题。

# 处理数据第二步:火车价格的数据处理
# 背景:目前火车所有坐席的价格在同一个单元格内,我们需要把不同坐席的价格放在不同的单元格内进行展示
# TODO 数据处理:分割数据
# START------------------------------------------------
split_columns = df['动车组票价'].str.split('/', expand=True)# 分割'动车组票价'、"火车组票价"列,并创建新的列名
split_columns.columns = ['动车组票价_二等座', '动车组票价_一等座']
df = df.join(split_columns).drop('动车组票价', axis=1)# 将新列加入原始 DataFrame并删除旧的'动车组票价'、"火车组票价"列
split_columns = df['火车组票价'].str.split('/', expand=True)
split_columns.columns = ['火车组票价_硬卧上', '火车组票价_硬卧中','火车组票价_硬卧下']
df = df.join(split_columns).drop('火车组票价', axis=1)
# END------------------------------------------------

数据分析_第三步:缺失值处理

在数据采集中,有部分火车的价格没有获取到,造成了火车的价格为null,为了为了保证数据分析数据真实性,我们将没有获取到火车价格的车次信息进行整行删除。

# 处理数据第三步:火车价格的缺失值处理
# 背景:有些火车数据非常扯淡!它没有获取到火车票价格,那我们就把没有获取到火车票价格的车次删除掉!
# TODO 数据处理:缺失值处理
# START------------------------------------------------
columns_of_interest = ['火车组票价_硬卧上', '火车组票价_硬卧中','火车组票价_硬卧下','动车组票价_二等座', '动车组票价_一等座'] #指定列名
for column in columns_of_interest:# 在csv中,缺失值是使用'-'代替,我们在指定列中将其替换为NaN以便更好处理缺失值
    df[column] = df[column].apply(lambda x: np.nan if x == ' - ' else x)

mask_all_na_in_columns = df[columns_of_interest].isna().all(axis=1)# 找出所有在指定列中全部是NaN的行
df = df[~mask_all_na_in_columns]# 删除那些在指定列中全部是NaN的行
# END------------------------------------------------

数据分析_第四步:数据清洗

在csv文件中读取的火车价格的数据类型为String(字符串)类型,为了实现后续的数学运算和数学分析,在此处我们需要将火车价格的数据类型从字符串转为float(浮点型)。否则会报错!

# 处理数据第四步:火车价格的数据清洗
# 背景:有些火车价格数据为string,我们需要把它们统一转换为float,才能进行下一步的数据最大/小值的计算,否则会报错!
# TODO 数据处理:火车价格的数据清洗
# START------------------------------------------------
df['动车组票价_二等座'] = pd.to_numeric(df['动车组票价_二等座'], errors='coerce')
df['动车组票价_一等座'] = pd.to_numeric(df['动车组票价_一等座'], errors='coerce')
df['火车组票价_硬卧上'] = pd.to_numeric(df['火车组票价_硬卧上'], errors='coerce')
df['火车组票价_硬卧中'] = pd.to_numeric(df['火车组票价_硬卧中'], errors='coerce')
df['火车组票价_硬卧下'] = pd.to_numeric(df['火车组票价_硬卧下'], errors='coerce')
# END------------------------------------------------

数据分析_第五步:数据排序与Xlsx文件写入

我们将根据火车的价格从高到低(AESC)进行排序,并将排序好的数据写入到/data/sort文件夹下(在写入前,应先判断此路径是否存在,如不存在应先创建)。通过上述操作可形成一个具有价格梯度的火车信息,帮助我们分析和查看。

注意:本次排序将通过动车组票价_二等座的票价数据为Key进行排序,其他字段不涉及排序。

# 处理数据第五步:根据火车价格进行AESC排序以及保存至XLSX文件中
# 背景:我们需要根据火车价格进行排序,形成一个具有价格梯度的火车信息,帮助我们分析和查看。
# 注意:在此处我们统一按照 动车组票价_二等座 进行排序,其他字段不涉及排序,将排序后的结果生成到 data/sort 文件夹下。
# TODO 数据处理:根据火车价格进行排序以及保存至XLSX文件中
# START------------------------------------------------
sorted_df = df.sort_values(by=['动车组票价_二等座'], ascending=False)
sorted_df.to_excel('data/sort/' + city_from + '-' + city_to + '.xlsx', index=False, engine='openpyxl')
# END------------------------------------------------

数据分析_第六步:重复值处理

在本项目中,数据本身没有重复的现象,但为确保项目实现过程与课程教学内容紧密对应,此处仍然进行重复值处理。

如果发现有重复值情况,则删除重复值,谨防对后续数学运算带来影响。

# 处理数据第六步:数据清洗,检测并删除重复值(重复数据)
# 背景:如果火车数据是重复的,那我们就需要删除重复数据,防止数据有多个造成数据不准确。
# TODO 数据处理:数据清洗,检测并删除重复值
# START------------------------------------------------
df.drop_duplicates(inplace=True)
# END------------------------------------------------

数据分析_第七步:运用max、min、mean进行数学统计

计算火车价格的最大值、最小值、平均值进行数据统计,方便我们查看数据。

在计算好数据之后,我们将这些数据写入到csv文件中(在文件的最后一行中再新起三行分别写入最小值、最大值、平均值)。

# 处理数据第七步:计算火车价格的最大值、最小值、平均值进行数据统计,将计算的结果新起一列写入csv文件中
# 背景:数据统计后,能发现乘坐哪个火车价格最实惠。
# TODO 数据处理:火车价格的数据统计
# START------------------------------------------------

new_row_data = {'车次': '车票价格总计_最大值',
                '动车组票价_二等座': df['动车组票价_二等座'].max(),
                '动车组票价_一等座': df['动车组票价_一等座'].max(),
                '火车组票价_硬卧上': df['火车组票价_硬卧上'].max(),
                '火车组票价_硬卧中': df['火车组票价_硬卧中'].max(),
                '火车组票价_硬卧下': df['火车组票价_硬卧下'].max(),
                }
df.loc[len(df)] = new_row_data  # 将最大值的数据添加到 df末尾
# ---分隔符---
new_row_data = {'车次': '车票价格总计_最小值',
                '动车组票价_二等座': df['动车组票价_二等座'].min(),
                '动车组票价_一等座': df['动车组票价_一等座'].min(),
                '火车组票价_硬卧上': df['火车组票价_硬卧上'].min(),
                '火车组票价_硬卧中': df['火车组票价_硬卧中'].min(),
                '火车组票价_硬卧下': df['火车组票价_硬卧下'].min(),
                }
df.loc[len(df)] = new_row_data  # 将最小值的数据添加到 df末尾
# ---分隔符---
new_row_data = {'车次': '车票价格总计_平均值',
                '动车组票价_二等座': df['动车组票价_二等座'].mean(),
                '动车组票价_一等座': df['动车组票价_一等座'].mean(),
                '火车组票价_硬卧上': df['火车组票价_硬卧上'].mean(),
                '火车组票价_硬卧中': df['火车组票价_硬卧中'].mean(),
                '火车组票价_硬卧下': df['火车组票价_硬卧下'].mean(),
                }
df.loc[len(df)] = new_row_data  # 将平均值的数据添加到 df末尾
# END------------------------------------------------

数据分析:运用describe进行数据输出并进行数据写入

我将前面七步执行完后,将通过print语句输出describe可更好查看具体概括数据。

# 输出df和desc
print(df)
print(df.describe())

输出后,我们需要将前面七步进行更改后的数据写到csv文件中。

# 将清理后的数据写回CSV文件
df.to_csv(file_path, index=False, encoding='gbk')

数据分析_第八步:合并csv文件

在前面的讲述中,我将每一条火车线路都单独创建了一个csv文件。于是我们将这些csv文件合并到一个csv文件里面,方便我们查看所有的数据。

# 处理数据:合并数据,将所有城市的火车数据合并到一个csv文件中
# 背景:我们需要将所有城市的火车数据合并到一个csv文件中,便于后续分析。
# TODO 数据处理:合并数据
# START------------------------------------------------
all_csv_files = glob.glob(os.path.join('data', "*.csv"))
all_csv_files_list = []
for csv_files_item in all_csv_files:
    all_csv_df = pd.read_csv(csv_files_item, encoding='gbk')
    all_csv_df = all_csv_df[
        ~(all_csv_df['车次'].isin(['车票价格总计_最大值','车票价格总计_最小值','车票价格总计_平均值']))
        ] # 这些值不用合并
    all_csv_files_list.append(all_csv_df)
all_cities_df = pd.concat(all_csv_files_list, axis=0, ignore_index=True)
all_cities_df.to_csv('所有城市的火车信息.csv', index=False, encoding='gbk')  # 将所有城市的火车数据写回一个csv文件
# END------------------------------------------------

[!CAUTION]

至此,数据分析已完成,下面将开始进行数学分析。

数学运算

经过数据分析的八个步骤后,我们的数据样本已经得到了大致的了解,下面我们通过数学运算来更好的进行数据层面的分析:

[!TIP]

在数学运算中,我们统一通过所有城市的火车信息.csv此文件进行数学运算的分析。

前提条件:读入csv文件

# 这里读入的是'所有城市的火车信息.csv',也就是合并后的数据进行检验
df = pd.read_csv('所有城市的火车信息.csv', encoding='gbk', na_values=' - ')

数学运算_第一步:假设检验(T检验)

假设检验可以用来比较不同类型的票价,看看各票价之间是否存在显著差异。

# 数学运算第一步:假设检验(T检验)
# 背景:假设检验可以用来比较不同类型的票价,看看是否存在显著差异。
# TODO 数学运算:假设检验
# START------------------------------------------------
t_stat, p_value = stats.ttest_ind(
    df['动车组票价_二等座'].dropna(),
    df['火车组票价_硬卧上'].dropna(),
    nan_policy='omit')# 比较动车组二等座和火车组硬卧上座的票价
print(f"假设检验检测_动车组票价_二等座 or 火车组票价_硬卧上 - T-statistic: {t_stat}, P-value: {p_value}")
# END------------------------------------------------

数学运算_第二步:分布拟合

我们可以拟合票价数据到常见的统计分布(例如正态分布),看数据是否符合这些分布。

# 数学运算第二步:分布拟合
# 背景:我们可以拟合票价数据到常见的统计分布(例如正态分布),看数据是否符合这些分布。
# TODO 数学运算:分布拟合
# START------------------------------------------------
params = stats.norm.fit(df['动车组票价_二等座'].dropna())  # 拟合正态分布
print(f"分布拟合检测_动车组票价_二等座 - 均值(Mean): {params[0]}, 标准差(Std): {params[1]}")
params = stats.norm.fit(df['火车组票价_硬卧上'].dropna())  # 拟合正态分布
print(f"分布拟合检测_火车组票价_硬卧上 - 均值(Mean): {params[0]}, 标准差(Std): {params[1]}")
params = stats.norm.fit(df['火车组票价_硬卧中'].dropna())  # 拟合正态分布
print(f"分布拟合检测_火车组票价_硬卧中 - 均值(Mean): {params[0]}, 标准差(Std): {params[1]}")
params = stats.norm.fit(df['火车组票价_硬卧下'].dropna())  # 拟合正态分布
print(f"分布拟合检测_火车组票价_硬卧下 - 均值(Mean): {params[0]}, 标准差(Std): {params[1]}")
# END------------------------------------------------

数学运算_第三步:方差分析(ANOVA)

利用数据中的方差数据,帮助我们更好地理解数据。

# 数学运算第三步:方差分析(ANOVA)
# 背景:利用方差,帮助我们更好地理解数据。
# TODO 数学运算:方差分析(ANOVA)
# START------------------------------------------------
# 方差分析(ANOVA)
f_stat, p_value = stats.f_oneway(
    df['动车组票价_二等座'].dropna(),
    df['动车组票价_一等座'].dropna()
)
print(f"方差分析(ANOVA) - F-statistic: {f_stat}, P-value: {p_value}")
# END------------------------------------------------

数学运算_第四步:数据的相关性分析

我们可以通过计算数据的相关系数,来判断数据之间的相关性。(此处火车和火车比较、动车和动车比较)

# 数学运算第四步:数据的相关性分析
# 背景:我们可以通过计算数据的相关系数,来判断数据之间的相关性。
# TODO 数学运算:数据的相关性分析
# START------------------------------------------------
# 相关性分析
corr_1 = df['动车组票价_二等座'].corr(df['动车组票价_一等座'])
corr_2 = df['火车组票价_硬卧上'].corr(df['火车组票价_硬卧中'])
print(f"动车组二等座和一等座票价的相关性: {corr_1}")
print(f"火车组硬卧上和硬卧中的票价相关性: {corr_2}")
# END------------------------------------------------
分类: 数据分析与深度学习数据采集与自动化处理 标签: 暂无标签

评论

暂无评论数据

暂无评论数据

目录