三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Python实战:机场客流数据分析系统构建与暑期客流暴涨现象解读

Python实战:机场客流数据分析系统构建与暑期客流暴涨现象解读

最近在分析国内机场客流数据时,发现了一个非常有意思的现象:上海浦东国际机场的单日客流量,竟然在某个时段达到了惊人的190.5万人次,一举反超了长期稳居榜首的广州白云机场。这背后,是包括浦东、白云、首都、深圳宝安等在内的全国11座主要机场客流量的集体“暴涨”。数据不会说谎,这波热潮清晰地指向了一个核心驱动力——即将到来的暑期出游需求正在被提前、集中地释放。对于开发者而言,这不仅是市场趋势,更是一个绝佳的数据分析实战场景。本文将带你从零开始,构建一个机场客流数据分析系统,涵盖数据爬取、清洗、存储、可视化及趋势预测的全流程,让你不仅能看懂这波“暴涨”背后的逻辑,更能亲手复现分析过程。

1. 背景与核心概念:为什么关注机场客流数据?

机场客流数据是反映区域经济活力、居民消费意愿和旅游业景气度的“晴雨表”。一次客流“暴涨”,背后可能是新航线开通、大型赛事会展、节假日效应或像暑期这样的季节性旅游高峰的叠加。

对于技术人,尤其是数据开发、数据分析领域的同学,这类公开的、时序性的、具有明显业务意义的数据,是绝佳的练手素材。通过分析它,我们可以实践以下技能:

  • 数据获取:学习如何从公开渠道(如民航局官网、数据平台)结构化地获取数据。
  • 时序分析:客流量是典型的时间序列数据,适合练习趋势分析、周期性(如周末效应、暑期效应)识别。
  • 异常检测:识别如“190.5万”这样的异常峰值,并分析其成因(是数据错误还是真实事件?)。
  • 数据可视化:将枯燥的数字转化为直观的图表,清晰展示“反超”、“暴涨”等动态。
  • 简单的预测模型:基于历史数据,尝试对短期客流进行预测。

本文将围绕“机场客流分析”这个主题,构建一个完整的分析管道。我们将使用 Python 作为主要语言,涉及pandas,requests,sqlalchemy,matplotlib等核心库。

2. 环境准备与版本说明

在开始编码前,请确保你的开发环境已就绪。以下版本为本文撰写时的常用稳定版本,你可以根据实际情况调整。

  • 操作系统:Windows 10/11, macOS, 或 Linux (如 Ubuntu 20.04+) 均可。
  • Python:版本 3.8 或以上。推荐使用 3.9+ 以获得更好的兼容性。
  • 开发工具:任选其一。
    • Jupyter Notebook / JupyterLab:适合交互式分析和可视化。
    • VS Code / PyCharm:适合完整的项目开发。
  • 数据库(可选):MySQL 8.0 或 PostgreSQL 13+,用于持久化存储数据。如果只想做内存分析,可跳过数据库部分。
  • 关键Python库
    # 在终端或命令提示符中执行以下命令安装 pip install pandas==1.5.3 numpy==1.24.3 requests==2.28.2 matplotlib==3.7.1 seaborn==0.12.2 # 如果需要连接数据库 pip install sqlalchemy==1.4.46 pymysql==1.0.2 # 连接MySQL # 或 pip install sqlalchemy==1.4.46 psycopg2-binary==2.9.6 # 连接PostgreSQL

项目结构建议如下,以便管理:

airport_traffic_analysis/ ├── data/ # 存放原始和清洗后的数据文件 │ ├── raw/ │ └── processed/ ├── src/ # 源代码 │ ├── crawler.py # 数据爬取模块 │ ├── cleaner.py # 数据清洗模块 │ ├── analyzer.py # 数据分析模块 │ └── visualizer.py # 数据可视化模块 ├── config.py # 配置文件(如数据库连接信息) ├── requirements.txt # 项目依赖列表 └── main.py # 主程序入口

3. 核心流程与原理拆解

一个完整的数据分析项目通常遵循ETL(抽取、转换、加载)或EDA(探索性数据分析)流程。结合我们的目标,核心流程如下:

3.1 数据获取 (Extract)

目标:获取全国主要机场的历史客流量数据。原理:数据可能来源于公开的API、数据网站,或是结构化的报表。由于直接获取实时官方数据可能受限,我们常采用两种方式:

  1. 模拟请求:使用requests库模拟浏览器请求,从提供数据的网站获取JSON或HTML格式的数据。
  2. 使用公开数据集:从Kaggle、天池、政府数据开放平台等寻找相关数据集。

关键点

  • 遵守robots.txt:在爬取前检查目标网站的robots.txt文件,尊重网站的爬虫协议。
  • 设置请求头:模拟真实浏览器访问,避免被反爬机制拦截。
  • 处理反爬:可能需要添加延时、使用代理IP或处理验证码(本文不涉及复杂反爬)。
  • 数据版权:仅将获取的数据用于个人学习与分析。

3.2 数据清洗与转换 (Transform)

目标:将原始杂乱数据转换为干净、可用于分析的结构化数据。原理:原始数据常包含缺失值、异常值、格式不一致等问题。

  • 处理缺失值:删除或填充(如用前后平均值、中位数填充)。
  • 处理异常值:利用统计方法(如3σ原则)或业务逻辑识别并处理。例如,某日客流量为0或负值,可能是数据错误。
  • 格式标准化:确保日期格式统一、数值类型正确、机场名称规范。
  • 数据重塑:可能需要进行数据透视(pivot)或融合(melt)以适应分析需求。

3.3 数据分析与可视化 (Analyze & Visualize)

目标:发现规律、识别趋势、定位异常。原理

  • 描述性统计:计算均值、中位数、标准差、分位数,了解数据整体分布。
  • 时序分析:绘制折线图观察长期趋势、季节性、周期性。
  • 对比分析:绘制柱状图、分组折线图,对比不同机场的客流,如“浦东 vs 广州”。
  • 相关性分析:计算机场间客流的相关系数,或分析客流与节假日、天气等因素的相关性。

3.4 数据存储 (Load - 可选)

目标:将清洗后的数据持久化,便于后续多次分析或构建应用。原理:使用关系型数据库(如MySQL)或文件(如CSV、Parquet)存储。

  • 数据库设计:设计合理的表结构,例如airport_traffic表包含字段:id,airport_name,date,passenger_count
  • 使用ORM:通过SQLAlchemy等ORM库操作数据库,使代码更清晰、安全。

4. 完整实战案例:分析机场客流数据

我们以一个模拟的公开数据集为例,演示全流程。假设我们已经从一个数据平台获取到了一个CSV文件raw_airport_traffic.csv

4.1 数据加载与初步探索

首先,创建src/analyzer.py文件,开始我们的分析。

# src/analyzer.py import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns # 设置中文显示和图表样式 plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] # 用来正常显示中文标签 plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号 sns.set_style("whitegrid") def load_and_explore_data(file_path): """ 加载数据并进行初步探索 """ # 1. 加载数据 try: df = pd.read_csv(file_path, parse_dates=['date']) # 假设有date列,并解析为日期类型 print("数据加载成功!") print(f"数据形状: {df.shape}") # (行数, 列数) except FileNotFoundError: print(f"错误:文件 {file_path} 未找到。") return None except Exception as e: print(f"加载数据时发生错误: {e}") return None # 2. 查看数据前几行和基本信息 print("\n--- 数据前5行 ---") print(df.head()) print("\n--- 数据基本信息 ---") print(df.info()) print("\n--- 描述性统计 ---") print(df.describe()) # 3. 检查缺失值 print("\n--- 缺失值统计 ---") missing_values = df.isnull().sum() print(missing_values[missing_values > 0]) # 只显示有缺失的列 return df if __name__ == "__main__": # 假设数据文件路径 data_path = "../data/raw/raw_airport_traffic.csv" df_raw = load_and_explore_data(data_path)

运行后,你可能会看到类似以下输出,这帮助我们了解数据全貌。

数据加载成功! 数据形状: (3650, 4) # 假设是10个机场1年的数据(365*10) --- 数据前5行 --- airport_name date passenger_count flight_count 0 上海浦东 2023-01-01 1250000 8500 1 广州白云 2023-01-01 1320000 8200 2 北京首都 2023-01-01 1150000 7800 3 深圳宝安 2023-01-01 980000 7200 ... --- 缺失值统计 --- passenger_count 5 flight_count 12 dtype: int64

4.2 数据清洗

创建src/cleaner.py来处理发现的问题,如缺失值、异常值。

# src/cleaner.py import pandas as pd def clean_traffic_data(df): """ 清洗机场客流数据 """ df_clean = df.copy() # 1. 处理缺失值 - 对于客流量,使用该机场该月(或该周)的平均值填充 # 先按机场分组,然后对客流量用该组的均值填充 if 'passenger_count' in df_clean.columns: df_clean['passenger_count'] = df_clean.groupby('airport_name')['passenger_count'].transform( lambda x: x.fillna(x.mean()) ) # 如果填充后还有缺失(比如该机场所有数据都缺失),则用全局均值填充 df_clean['passenger_count'].fillna(df_clean['passenger_count'].mean(), inplace=True) # 2. 处理异常值 - 使用分位数法检测 Q1 = df_clean['passenger_count'].quantile(0.25) Q3 = df_clean['passenger_count'].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # 标记异常值,但不直接删除,而是进行修正(例如,用上下边界值替换) outliers = (df_clean['passenger_count'] < lower_bound) | (df_clean['passenger_count'] > upper_bound) print(f"检测到 {outliers.sum()} 个客流量异常值。") # 修正异常值为边界值 df_clean.loc[df_clean['passenger_count'] < lower_bound, 'passenger_count'] = lower_bound df_clean.loc[df_clean['passenger_count'] > upper_bound, 'passenger_count'] = upper_bound # 3. 确保数据类型正确 df_clean['passenger_count'] = df_clean['passenger_count'].astype('int64') df_clean['date'] = pd.to_datetime(df_clean['date']) # 4. 按日期和机场排序 df_clean.sort_values(by=['airport_name', 'date'], inplace=True) df_clean.reset_index(drop=True, inplace=True) print("数据清洗完成!") return df_clean if __name__ == "__main__": # 假设从analyzer模块导入原始数据 from analyzer import load_and_explore_data df_raw = load_and_explore_data("../data/raw/raw_airport_traffic.csv") if df_raw is not None: df_clean = clean_traffic_data(df_raw) # 保存清洗后的数据 df_clean.to_csv("../data/processed/cleaned_airport_traffic.csv", index=False) print("清洗后的数据已保存。")

4.3 核心分析:识别“反超”与“暴涨”

现在,我们进入最有趣的部分。创建src/visualizer.py来生成洞察。

# src/visualizer.py import pandas as pd import matplotlib.pyplot as plt import seaborn as sns from matplotlib.dates import DateFormatter def analyze_top_airports_trend(df, top_n=5, start_date='2023-06-01', end_date='2023-08-31'): """ 分析特定时段内顶级机场的客流趋势,重点识别‘反超’现象。 """ # 1. 筛选时间段 mask = (df['date'] >= start_date) & (df['date'] <= end_date) df_period = df.loc[mask].copy() if df_period.empty: print(f"在 {start_date} 到 {end_date} 期间没有数据。") return # 2. 计算该时段内各机场的总客流,并选出Top N airport_total = df_period.groupby('airport_name')['passenger_count'].sum().sort_values(ascending=False) top_airports = airport_total.head(top_n).index.tolist() print(f"在 {start_date} 至 {end_date} 期间,客流量 Top {top_n} 的机场是:{top_airports}") # 3. 筛选出Top机场的数据 df_top = df_period[df_period['airport_name'].isin(top_airports)] # 4. 绘制每日客流趋势图 plt.figure(figsize=(16, 8)) for airport in top_airports: df_airport = df_top[df_top['airport_name'] == airport] plt.plot(df_airport['date'], df_airport['passenger_count'], marker='o', label=airport, linewidth=2) plt.title(f'暑期({start_date} 至 {end_date})Top {top_n} 机场每日客流量趋势对比', fontsize=16, fontweight='bold') plt.xlabel('日期', fontsize=12) plt.ylabel('客流量(万人次)', fontsize=12) # 将y轴刻度调整为“万”单位 ax = plt.gca() y_ticks = ax.get_yticks() ax.set_yticklabels([f'{int(y/10000)}' for y in y_ticks]) plt.gca().xaxis.set_major_formatter(DateFormatter('%m-%d')) # 格式化日期显示 plt.legend(title='机场') plt.grid(True, linestyle='--', alpha=0.7) plt.tight_layout() plt.savefig('../output/top_airports_trend_summer.png', dpi=300) plt.show() # 5. 识别具体的“反超”日 # 我们找出上海浦东客流量超过广州白云的日期 df_pvg = df_top[df_top['airport_name'] == '上海浦东'].set_index('date')['passenger_count'] df_can = df_top[df_top['airport_name'] == '广州白云'].set_index('date')['passenger_count'] # 对齐日期索引,进行逐日比较 comparison = pd.DataFrame({'PVG': df_pvg, 'CAN': df_can}) comparison = comparison.dropna() # 去除任一机场数据缺失的日期 overtake_days = comparison[comparison['PVG'] > comparison['CAN']] if not overtake_days.empty: print(f"\n发现上海浦东客流量反超广州白云的日期:") print(overtake_days.index.strftime('%Y-%m-%d').tolist()) # 找出反超幅度最大的一天 overtake_days['diff'] = overtake_days['PVG'] - overtake_days['CAN'] max_overtake_day = overtake_days['diff'].idxmax() max_overtake_value = overtake_days.loc[max_overtake_day, 'diff'] print(f"其中,反超幅度最大的一天是 {max_overtake_day.strftime('%Y-%m-%d')},浦东比白云多 {max_overtake_value/10000:.1f} 万人次。") # 检查是否达到“190.5万”级别的峰值 pvg_peak = df_pvg.max() print(f"上海浦东在该时段内的峰值客流量为:{pvg_peak/10000:.1f} 万人次") else: print("\n在该时段内,未发现上海浦东客流量反超广州白云的情况。") def detect_traffic_surge(df, surge_threshold_pct=30): """ 检测客流‘暴涨’(环比大幅增长)的日期和机场。 surge_threshold_pct: 定义‘暴涨’的阈值,例如日环比增长超过30% """ df = df.copy() df.sort_values(by=['airport_name', 'date'], inplace=True) # 计算每个机场每天的环比增长率 df['daily_growth'] = df.groupby('airport_name')['passenger_count'].pct_change() * 100 # 找出增长率超过阈值的记录 surge_events = df[df['daily_growth'] > surge_threshold_pct] if not surge_events.empty: print(f"\n检测到日环比增长超过 {surge_threshold_pct}% 的‘暴涨’事件共 {len(surge_events)} 起:") for _, row in surge_events.head(10).iterrows(): # 只显示前10条 print(f" 日期:{row['date'].strftime('%Y-%m-%d')},机场:{row['airport_name']}," f"客流量:{row['passenger_count']/10000:.1f}万,增长率:{row['daily_growth']:.1f}%") # 可以进一步按机场或日期聚合分析 surge_by_airport = surge_events.groupby('airport_name').size().sort_values(ascending=False) print(f"\n‘暴涨’事件按机场分布:\n{surge_by_airport}") else: print(f"\n未检测到日环比增长超过 {surge_threshold_pct}% 的‘暴涨’事件。") if __name__ == "__main__": # 加载清洗后的数据 df_clean = pd.read_csv("../data/processed/cleaned_airport_traffic.csv", parse_dates=['date']) # 分析暑期趋势 analyze_top_airports_trend(df_clean, top_n=5, start_date='2023-07-01', end_date='2023-08-31') # 检测暴涨事件 detect_traffic_surge(df_clean, surge_threshold_pct=25)

运行这段代码,你将得到清晰的趋势图,并能在控制台看到类似下面的分析结果:

在 2023-07-01 至 2023-08-31 期间,客流量 Top 5 的机场是:['广州白云', '上海浦东', '北京首都', '深圳宝安', '成都天府'] 发现上海浦东客流量反超广州白云的日期: ['2023-07-15', '2023-07-22', '2023-08-05', '2023-08-19'] 其中,反超幅度最大的一天是 2023-08-05,浦东比白云多 15.3 万人次。 上海浦东在该时段内的峰值客流量为:192.1 万人次 检测到日环比增长超过 25% 的‘暴涨’事件共 47 起: 日期:2023-07-01,机场:上海浦东,客流量:165.2万,增长率:32.5% 日期:2023-07-08,机场:广州白云,客流量:158.7万,增长率:28.1% ... ‘暴涨’事件按机场分布: 机场名称 上海浦东 12 广州白云 10 北京首都 8 ...

4.4 数据存储(数据库示例)

为了持久化分析结果,我们可以将清洗后的数据存入MySQL数据库。

# config.py (配置文件,注意不要上传到版本库) DB_CONFIG = { 'host': 'localhost', 'port': 3306, 'user': 'your_username', 'password': 'your_password', # 强烈建议从环境变量读取 'database': 'airport_traffic_db', 'charset': 'utf8mb4' }
# src/db_handler.py import pandas as pd from sqlalchemy import create_engine, text from sqlalchemy.exc import SQLAlchemyError import config # 导入配置文件 def save_to_database(df, table_name='airport_traffic_daily'): """ 将DataFrame保存到MySQL数据库 """ # 构建数据库连接字符串 db_url = f"mysql+pymysql://{config.DB_CONFIG['user']}:{config.DB_CONFIG['password']}@{config.DB_CONFIG['host']}:{config.DB_CONFIG['port']}/{config.DB_CONFIG['database']}?charset={config.DB_CONFIG['charset']}" try: engine = create_engine(db_url, echo=False) # echo=True 会打印SQL语句,调试时可用 # 将数据写入数据库,如果表存在则替换 df.to_sql(name=table_name, con=engine, if_exists='replace', index=False) print(f"数据成功写入数据库表 `{table_name}`。") # 验证写入:读取前5行 with engine.connect() as conn: result = conn.execute(text(f"SELECT * FROM {table_name} LIMIT 5")) print("验证数据(前5行):") for row in result: print(row) except SQLAlchemyError as e: print(f"数据库操作失败: {e}") except Exception as e: print(f"发生未知错误: {e}") finally: if 'engine' in locals(): engine.dispose() if __name__ == "__main__": df_clean = pd.read_csv("../data/processed/cleaned_airport_traffic.csv", parse_dates=['date']) # 确保日期列是字符串格式,以便数据库存储 df_clean['date'] = df_clean['date'].dt.strftime('%Y-%m-%d') save_to_database(df_clean)

5. 常见问题与排查思路

在实践上述流程时,你可能会遇到以下问题:

问题现象常见原因解决思路
pd.read_csv报编码错误CSV文件编码不是默认的utf-8(可能是gbkgb2312)。尝试pd.read_csv(file_path, encoding='gbk')encoding='gb2312'。用chardet库检测文件编码。
图表无法显示中文系统或 matplotlib 未配置中文字体。如本文代码所示,在绘图前设置plt.rcParams['font.sans-serif']。或者下载中文字体(如 SimHei.ttf)并指定路径。
连接数据库失败1. 数据库服务未启动。
2. 用户名/密码错误。
3. 网络或防火墙问题。
4. 未安装对应的数据库驱动(如pymysql)。
1. 检查 MySQL/PostgreSQL 服务状态。
2. 核对config.py中的凭证。
3. 尝试用命令行工具(如mysql -u root -p)连接。
4. 执行pip install pymysql
数据爬取被屏蔽请求频率过高或缺少请求头,触发网站反爬机制。1. 在requests.get()中添加合理的headers(模拟浏览器)。
2. 在请求间添加随机延时time.sleep(random.uniform(1,3))
3. 考虑使用requests.Session()
分组或聚合结果为空分组键(groupby)存在大量 NaN 值,或筛选条件过于严格导致无数据。1. 检查用于分组的列是否存在缺失值:df['group_column'].isnull().sum()
2. 放宽筛选条件,或先处理缺失值。
趋势图中日期显示混乱日期列未被正确识别为datetime类型。使用pd.to_datetime(df['date_column'])强制转换,或在read_csv时指定parse_dates=['date_column']

6. 最佳实践与工程建议

将个人数据分析脚本提升到可维护、可复用的工程级别,需要注意以下几点:

  1. 配置与密钥管理

    • 绝对不要将数据库密码、API密钥等敏感信息硬编码在代码中。
    • 使用配置文件(如config.pyconfig.iniconfig.yaml),并将其加入.gitignore
    • 更安全的方式是使用环境变量。例如:import os; db_pass = os.getenv('DB_PASSWORD')
  2. 代码模块化与复用

    • 如本文所示,将不同功能拆分为独立模块(crawler,cleaner,analyzer,visualizer)。
    • main.py中组织主流程,使逻辑清晰。
    • 将通用函数(如数据库连接、日志记录)放入utils模块。
  3. 错误处理与日志记录

    • 对可能失败的操作(如网络请求、数据库连接、文件读写)使用try...except进行包裹。
    • 使用 Python 内置的logging模块替代print语句,可以方便地控制日志级别、输出到文件等。
    import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) logger.info("数据加载开始...")
  4. 数据备份与版本控制

    • 原始数据、清洗后数据、分析结果图表都应保存在项目目录中,并考虑使用.gitignore忽略大型数据文件。
    • 对数据处理的关键步骤(如清洗逻辑)进行注释,或使用Jupyter Notebook的单元格记录分析思路。
  5. 性能考虑

    • 处理大规模数据时,避免在DataFrame上使用低效的循环。优先使用 Pandas 的向量化操作或apply函数。
    • 如果数据量极大(GB级别),考虑使用DaskPySpark,或将数据存入数据库后使用 SQL 进行初步聚合。
  6. 分析结论的可视化与报告

    • 图表颜色应易于区分,并考虑色盲用户的体验。
    • 为图表添加清晰的标题、轴标签和图例。
    • 重要的分析结论(如“浦东于8月5日反超白云,峰值达192万”)除了在图中体现,也应在代码输出或最终报告中进行文字总结。

通过这个完整的项目,你不仅能够复现“浦东反超广州”、“11座机场暴涨”的数据分析过程,更重要的是掌握了一套从数据获取到洞察呈现的标准化方法。这套方法可以迁移到任何类似的时序数据分析场景中,例如分析电商销售数据、APP日活数据、城市气温变化等。

← 返回列表