File size: 10,695 Bytes
2307f32
 
 
 
 
76afe6d
 
 
 
2307f32
 
76afe6d
2307f32
61a6b87
2307f32
76afe6d
 
f072311
f6b784e
2307f32
 
 
8846ec1
2307f32
 
f6b784e
 
 
 
2307f32
 
76afe6d
 
 
f6b784e
 
2307f32
76afe6d
 
 
 
 
 
 
 
 
 
 
 
 
f6b784e
2307f32
 
f6b784e
 
 
2307f32
f6b784e
2307f32
f6b784e
2307f32
 
 
f6b784e
2307f32
f6b784e
 
 
 
 
76afe6d
f6b784e
 
 
2307f32
f6b784e
2307f32
 
 
 
f6b784e
 
2307f32
f6b784e
2307f32
 
 
 
f6b784e
 
 
 
 
 
76afe6d
f6b784e
 
 
 
 
2307f32
461075b
f6b784e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461075b
2307f32
461075b
 
 
 
 
76afe6d
461075b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f072311
f6b784e
461075b
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
"""
TODO 
"""
# 首先计算相对路径
from pathlib import Path
import os
import urllib.request, zipfile
import streamlit as st

this_file = Path(__file__).resolve()
this_directory = this_file.parent
# 数据集文件夹路径
data_cangzhou_folder = this_directory / "data/Cangzhou"
data_static_folder = data_cangzhou_folder / "Static" # windows linux差异,不要写错了




# 数据文件夹路径(静态数据)
data_folder = data_static_folder

# 然后
import streamlit as st
import pandas as pd
from datetime import datetime
import plotly.graph_objects as go
import plotly.express as px

# 设置页面配置为宽屏模式,以便能同时显示三个图表
st.set_page_config(layout="wide")




# 设置应用标题,参考数据集介绍,反映作业要求
st.title("S&M-HSTPM2d5数据集可视化——清华大学数据可视化课程作业1")


# 如果 data/Cangzhou 目录不存在,则下载并解压数据集
if not (data_cangzhou_folder).exists():
    st.info("Cangzhou 数据集不存在,正在下载中,请耐心等待……")
    url = "https://zenodo.org/records/4028130/files/Cangzhou.zip?download=1"
    local_zip = this_directory / "Cangzhou.zip"
    urllib.request.urlretrieve(url, str(local_zip))
    with zipfile.ZipFile(str(local_zip), "r") as zip_ref:
         zip_ref.extractall(path=str(this_directory / "data"))
    st.success("Cangzhou 数据集下载并解压完成!")
    st.rerun()

st.markdown("叶璨铭,2024214500,[email protected]")
# 创建三个等宽的列,分别展示图(a)、图(b)和图(c)
col1, col2, col3 = st.columns(3)

@st.cache_data
def load_and_filter_data(file_path, start_time, end_time):
    # 根据CSV格式,第一列作为时间戳(无列名),后续列依次为pm2d5、lat、lon
    df = pd.read_csv(file_path, header=0, names=['timestamp', 'pm2d5', 'lat', 'lon'])
    # 转换为datetime格式
    df['timestamp'] = pd.to_datetime(df['timestamp'])
    # 过滤指定区间数据
    filtered_df = df[(df['timestamp'] >= start_time) & (df['timestamp'] <= end_time)]
    return filtered_df

# 图(a): PM2.5随时间变化折线图(静态传感器数据)
with col1:
    st.header("(a)折线图:展示 PM2.5 浓度水平随时间的变化")
    with st.expander("绘制要求"):
        st.markdown("1. 使用 static 文件夹中的 .csv 文件,并筛选出时间戳在 2019-01-01 00:00:00 到 2019-01-01 12:00:00 之间的数据。")
        st.markdown("2. X 轴和 Y 轴分别表示时间和 PM2.5 浓度水平。")
        st.markdown("3. 为每个静态传感器绘制一条折线,并用不同颜色进行区分。")
        
    # 定义过滤时间段
    start_dt = datetime(2019, 1, 1, 0, 0, 0)
    end_dt = datetime(2019, 1, 1, 12, 0, 0)
    
    # 获取静态数据文件夹中CSV文件
    csv_files = [f for f in os.listdir(data_folder) if f.endswith('.csv')]
    sensor_data = {}
    for file in csv_files:
        file_path = os.path.join(data_folder, file)
        sensor_name = file.split('.')[0]
        sensor_data[sensor_name] = load_and_filter_data(file_path, start_dt, end_dt)
    
    # 整理数据:以时间戳为索引,每个传感器的PM2.5为一列
    chart_data = pd.DataFrame()
    for sensor, data in sensor_data.items():
        chart_data[sensor] = data.set_index('timestamp')['pm2d5']
    
    # 绘制折线图(添加图表标题及图例标签)
    fig_line = px.line(chart_data, 
                       title="PM2.5随时间变化折线图",
                       labels={"variable": "传感器", "value": "PM2.5", "index": "时间"})
    fig_line.update_layout(xaxis_title="时间", yaxis_title="PM2.5水平")
    st.plotly_chart(fig_line, use_container_width=True)
    
    with st.expander("详细说明"):
        st.markdown("**描述:** 本图展示了各静态传感器在2019年1月1日0:00至12:00期间的PM2.5浓度随时间变化的趋势。横轴表示时间,纵轴表示PM2.5数值,不同折线代表不同传感器的数据。")
        st.markdown("**解读:** 曲线波动反映了空气质量的时段变化,峰值可能预示短期污染事件,而持续低值表明空气较为清洁。传感器数据对比有助于区域污染差异的分析。")
    
# 图(b): 车辆移动散点图(移动传感器数据)
with col2:
    st.header("(b)散点图:展示车辆搭载移动传感器在城市中的移动情况")
    with st.expander("绘制要求"):
        st.markdown("1. 使用 mobile 文件夹中的 .csv 文件,并筛选出时间戳在 2019-01-02 10:00:00 到 2019-01-02 10:20:00 之间的数据。")
        st.markdown("2. X 轴和 Y 轴分别表示经度和纬度。")
        st.markdown("3. 使用散点图展示车辆传感器的位置,用不同颜色区分各传感器,并通过调整透明度(早期数据更透明)表达时间演变。")
        
    # 定义mobile数据文件夹路径
    mobile_folder = str(this_directory / "data/Cangzhou/Mobile")
    csv_files_mobile = [f for f in os.listdir(mobile_folder) if f.endswith('.csv')]
    mobile_sensor_data = {}
    start_mobile = datetime(2019, 1, 2, 10, 0, 0)
    end_mobile = datetime(2019, 1, 2, 10, 20, 0)
    
    for file in csv_files_mobile:
        sensor_name = file.split('.')[0]
        file_path = os.path.join(mobile_folder, file)
        df = load_and_filter_data(file_path, start_mobile, end_mobile)
        if not df.empty:
            mobile_sensor_data[sensor_name] = df
            
    # 使用Plotly绘制散点图并根据时间调整透明度
    fig2 = go.Figure()
    colors = px.colors.qualitative.Plotly
    def hex_to_rgba(hex_color, alpha):
        hex_color = hex_color.lstrip('#')
        r = int(hex_color[0:2], 16)
        g = int(hex_color[2:4], 16)
        b = int(hex_color[4:6], 16)
        return f"rgba({r}, {g}, {b}, {alpha})"
    
    total_time = (end_mobile - start_mobile).total_seconds()
    sensor_index = 0
    for sensor, data in mobile_sensor_data.items():
        base_color = colors[sensor_index % len(colors)]
        sensor_index += 1
        custom_colors = []
        for ts in data['timestamp']:
            dt_seconds = (ts - start_mobile).total_seconds()
            normalized = dt_seconds / total_time if total_time > 0 else 0
            # 透明度:早期数据较透明(alpha值较低),后期较不透明
            alpha = 0.3 + 0.7 * normalized
            custom_colors.append(hex_to_rgba(base_color, alpha))
        fig2.add_trace(go.Scatter(
            x = data['lon'],
            y = data['lat'],
            mode = 'markers',
            marker = dict(color = custom_colors, size = 10),
            name = sensor
        ))
    
    fig2.update_layout(
        xaxis_title="经度",
        yaxis_title="纬度",
        title="车辆移动散点图"
    )
    
    st.plotly_chart(fig2, use_container_width=True)

    with st.expander("详细说明"):
        st.markdown("**描述:** 本图利用Mobile文件夹中的CSV数据,在2019年1月2日10:00至10:20期间展示车辆传感器的位置分布。横轴表示经度,纵轴表示纬度,不同颜色代表不同传感器。")
        st.markdown("**解读:** 通过调整散点透明度(早期数据较透明),图中显示了车辆移动的时间演变趋势,为探索城市中车辆行驶路径提供依据。")

# 图(c): 3D直方图——展示整个城市在特定时段内的PM2.5分布
with col3:
    st.header("(c)3D直方图:展示整个城市在特定时段内的PM2.5分布")
    with st.expander("绘制要求"):
        st.markdown("1. 使用 mobile 和 static 文件夹中的 .csv 文件,并筛选出时间戳在 2019-01-01 09:00:00 到 2019-01-01 09:10:00 之间的数据。")
        st.markdown("2. 将数据的GPS坐标转换为网格坐标(例如采用 0.01 度的分辨率),并在相同网格内聚合数据。")
        st.markdown("3. X 轴、Y 轴和 Z 轴分别表示经度、纬度和PM2.5数值。")
        
    # 定义过滤时间段
    start_c = datetime(2019, 1, 1, 9, 0, 0)
    end_c = datetime(2019, 1, 1, 9, 10, 0)
    
    # 读取 static 文件夹中的 CSV 文件数据
    csv_files_static = [f for f in os.listdir(data_folder) if f.endswith('.csv')]
    data_list = []
    for file in csv_files_static:
        file_path = os.path.join(data_folder, file)
        df = load_and_filter_data(file_path, start_c, end_c)
        if not df.empty:
            data_list.append(df)
    
    # 读取 mobile 文件夹中的 CSV 文件数据
    csv_files_mobile = [f for f in os.listdir(str(this_directory / "data/Cangzhou/Mobile")) if f.endswith('.csv')]
    mobile_folder = str(this_directory / "data/Cangzhou/Mobile")
    for file in csv_files_mobile:
        file_path = os.path.join(mobile_folder, file)
        df = load_and_filter_data(file_path, start_c, end_c)
        if not df.empty:
            data_list.append(df)
    
    # 合并所有数据
    if data_list:
        df_all = pd.concat(data_list, ignore_index=True)
    else:
        df_all = pd.DataFrame(columns=["timestamp", "pm2d5", "lat", "lon"])
    
    # 将 GPS 坐标转换为网格坐标(采用 0.01 度分辨率)
    resolution = 0.01
    df_all["lon_grid"] = df_all["lon"].round(2)
    df_all["lat_grid"] = df_all["lat"].round(2)
    
    # 聚合每个网格内的PM2.5数据(计算平均值)
    df_group = df_all.groupby(["lon_grid", "lat_grid"]).agg({"pm2d5": "mean"}).reset_index()
    
    # 绘制3D直方图:由于 Plotly 不支持原生 Bar3d,这里采用 3D 表面图展示网格数据
    # 将聚合数据转换为适合表面图的矩阵
    if not df_group.empty:
        pivot = df_group.pivot(index="lat_grid", columns="lon_grid", values="pm2d5")
        pivot = pivot.fillna(0)
        # 创建3D表面图,x轴为经度、y轴为纬度、z轴为PM2.5平均值
        fig3 = go.Figure(data=[go.Surface(z=pivot.values, x=pivot.columns, y=pivot.index, colorscale='Viridis')])
    else:
        fig3 = go.Figure()
    fig3.update_layout(
        scene=dict(
            xaxis_title="经度",
            yaxis_title="纬度",
            zaxis_title="PM2.5"
        ),
        title="PM2.5分布3D表面图"
    )
    st.plotly_chart(fig3, use_container_width=True)

    with st.expander("详细说明"):
        st.markdown("**描述:** 本图通过汇总 mobile 和 static 两种传感器数据,展示了 2019-01-01 09:00 至 09:10 期间整个城市内PM2.5浓度的分布。")
        st.markdown("**解读:** 基于GPS坐标转换为网格坐标后,对相同网格中的数据进行聚合,直观展示不同区域的空气质量水平,从而为进一步的环境分析提供参考。")