OttoYu commited on
Commit
76c013a
·
verified ·
1 Parent(s): 741d32f

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +182 -0
  2. requirements.txt +110 -0
app.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import requests
5
+ from bs4 import BeautifulSoup
6
+ import csv
7
+ from datetime import datetime
8
+ import calendar
9
+
10
+ color_map = {
11
+ "Shek Pik": "blue",
12
+ "Quarry Bay": "red"
13
+ }
14
+
15
+ def get_end_date_from_month(month_str):
16
+ try:
17
+ dt = datetime.strptime(month_str, "%Y-%m")
18
+ except ValueError:
19
+ raise ValueError("Invalid format. Please use YYYY-MM (e.g., '2023-07')")
20
+ last_day = calendar.monthrange(dt.year, dt.month)[1]
21
+ return dt.year, dt.month, f"{dt.year}-{dt.month:02d}-{last_day:02d}"
22
+
23
+ def fetch_measured_data(station_name, endtime, period="30"):
24
+ station_codes = {"Quarry Bay": "quar", "Shek Pik": "shek"}
25
+ code = station_codes.get(station_name)
26
+ if not code:
27
+ raise ValueError(f"Invalid station name: {station_name}")
28
+ if len(endtime) == 10:
29
+ endtime_full = endtime + " 23:59:59"
30
+ else:
31
+ endtime_full = endtime
32
+ url = f"https://www.ioc-sealevelmonitoring.org/bgraph.php?code={code}&output=tab&period={period}&endtime={endtime_full}"
33
+ try:
34
+ response = requests.get(url)
35
+ response.raise_for_status()
36
+ except requests.RequestException as e:
37
+ raise RuntimeError(f"Error fetching data: {e}")
38
+ soup = BeautifulSoup(response.text, 'html.parser')
39
+ table = soup.find('table')
40
+ if not table:
41
+ raise ValueError(f"No data table found in HTML for station {station_name} at {endtime_full}")
42
+ rows = table.find_all('tr')
43
+ data = [[col.get_text(strip=True) for col in row.find_all(['td', 'th'])] for row in rows]
44
+ output_csv = f"{code}_tide_data.csv"
45
+ with open(output_csv, 'w', newline='', encoding='utf-8') as f:
46
+ writer = csv.writer(f)
47
+ writer.writerows(data)
48
+ return output_csv
49
+
50
+ def load_measured_csv(file_path, station_name):
51
+ df = pd.read_csv(file_path)
52
+ df.columns = df.columns.str.strip()
53
+ df['Time (UTC)'] = pd.to_datetime(df['Time (UTC)'], errors='coerce')
54
+ df = df.dropna(subset=['Time (UTC)'])
55
+ df['Time (UTC+8)'] = df['Time (UTC)'].dt.tz_localize('UTC').dt.tz_convert('Asia/Hong_Kong')
56
+ df['Station'] = station_name
57
+ return df[['Time (UTC+8)', 'flt(m)', 'Station']].rename(columns={'flt(m)': 'Measured'})
58
+
59
+ def fetch_hko_tide_data(url, station_name, year):
60
+ try:
61
+ response = requests.get(url)
62
+ response.raise_for_status()
63
+ except requests.RequestException:
64
+ return None
65
+ soup = BeautifulSoup(response.text, 'html.parser')
66
+ rows = soup.find_all('tr')[1:]
67
+ data = []
68
+ for row in rows:
69
+ cols = [td.get_text(strip=True) for td in row.find_all(['td', 'th'])]
70
+ if len(cols) >= 26:
71
+ mm, dd = cols[0], cols[1]
72
+ for hour in range(24):
73
+ tide_str = cols[hour + 2]
74
+ if tide_str == '':
75
+ continue
76
+ try:
77
+ tide = float(tide_str)
78
+ dt = datetime(year, int(mm), int(dd), hour)
79
+ data.append({'Datetime': dt, 'Tide Height (m)': tide, 'Station': station_name})
80
+ except ValueError:
81
+ continue
82
+ return pd.DataFrame(data)
83
+
84
+ def tide_analysis_for_month_gradio(month_str):
85
+ logs = []
86
+
87
+ if not month_str:
88
+ return "Please enter a month in YYYY-MM format.", None, None, None
89
+
90
+ try:
91
+ logs.append(f"Parsing input month: {month_str}")
92
+ year, month, end_date = get_end_date_from_month(month_str)
93
+ logs.append(f"End date calculated: {end_date}")
94
+
95
+ # Fetch measured data
96
+ logs.append("Fetching measured data for Shek Pik...")
97
+ file_shek = fetch_measured_data("Shek Pik", end_date)
98
+ logs.append("Fetching measured data for Quarry Bay...")
99
+ file_quar = fetch_measured_data("Quarry Bay", end_date)
100
+
101
+ logs.append("Loading and processing measured CSV data...")
102
+ df_shek = load_measured_csv(file_shek, "Shek Pik")
103
+ df_quar = load_measured_csv(file_quar, "Quarry Bay")
104
+ df_measured = pd.concat([df_shek, df_quar], ignore_index=True)
105
+ min_time = df_measured['Time (UTC+8)'].min()
106
+ max_time = df_measured['Time (UTC+8)'].max()
107
+ logs.append(f"Measured data range: {min_time} to {max_time}")
108
+
109
+ # Fetch predicted tide data
110
+ logs.append("Fetching predicted tide data from HKO...")
111
+ url_quar = f"https://www.hko.gov.hk/tide/QUBtextPH{year}.htm"
112
+ url_shek = f"https://www.hko.gov.hk/tide/SPWtextPH{year}.htm"
113
+ df_pred_quar = fetch_hko_tide_data(url_quar, "Quarry Bay", year)
114
+ df_pred_shek = fetch_hko_tide_data(url_shek, "Shek Pik", year)
115
+
116
+ if df_pred_quar is None or df_pred_shek is None:
117
+ logs.append("Failed to fetch predicted tide data.")
118
+ return "\n".join(logs), None, None, None
119
+
120
+ logs.append("Processing predicted tide data...")
121
+ df_pred = pd.concat([df_pred_quar, df_pred_shek], ignore_index=True)
122
+ df_pred['Time (UTC+8)'] = pd.to_datetime(df_pred['Datetime']).dt.tz_localize('Asia/Hong_Kong')
123
+ df_pred = df_pred.rename(columns={'Tide Height (m)': 'Predicted'})
124
+ df_pred = df_pred[(df_pred['Time (UTC+8)'] >= min_time) & (df_pred['Time (UTC+8)'] <= max_time)]
125
+
126
+ logs.append("Generating plot for predicted tide...")
127
+ fig_pred = px.line(df_pred, x='Time (UTC+8)', y='Predicted', color='Station',
128
+ title='Predicted Tide',
129
+ labels={'Predicted': 'Tide Height (m)', 'Time (UTC+8)': 'Time (UTC+8)'},
130
+ color_discrete_map=color_map)
131
+ fig_pred.update_traces(mode='lines+markers')
132
+
133
+ logs.append("Generating plot for measured tide...")
134
+ fig_meas = px.line(df_measured, x='Time (UTC+8)', y='Measured', color='Station',
135
+ title='Measured Tide',
136
+ labels={'Measured': 'Tide Height (m)', 'Time (UTC+8)': 'Time (UTC+8)'},
137
+ color_discrete_map=color_map)
138
+ fig_meas.update_traces(mode='lines+markers')
139
+
140
+ logs.append("Calculating and plotting residuals...")
141
+ df_merged = pd.merge(df_measured, df_pred[['Time (UTC+8)', 'Predicted', 'Station']],
142
+ on=['Time (UTC+8)', 'Station'], how='inner')
143
+ df_merged['Residual'] = df_merged['Measured'] - df_merged['Predicted']
144
+ fig_resid = px.line(df_merged, x='Time (UTC+8)', y='Residual', color='Station',
145
+ title='Tide Residuals (Measured - Predicted)',
146
+ labels={'Residual': 'Residual (m)', 'Time (UTC+8)': 'Time (UTC+8)'},
147
+ color_discrete_map=color_map)
148
+ fig_resid.update_traces(mode='lines+markers')
149
+
150
+ logs.append("Analysis completed successfully.")
151
+ return "\n".join(logs), fig_pred, fig_meas, fig_resid
152
+
153
+ except Exception as e:
154
+ logs.append(f"Error during processing: {e}")
155
+ return "\n".join(logs), None, None, None
156
+
157
+ # Gradio UI
158
+ with gr.Blocks() as demo:
159
+ gr.Markdown("## Tide Time Series Analysis by Month")
160
+
161
+ # --- First Row: Controls ---
162
+ with gr.Row():
163
+ month_input = gr.Textbox(label="Enter Month (YYYY-MM)", placeholder="e.g. 2023-07")
164
+ run_btn = gr.Button("Run Analysis")
165
+
166
+ # --- Second Row: Plot Area ---
167
+ with gr.Row():
168
+ with gr.Column():
169
+ with gr.Row():
170
+ plot_meas = gr.Plot(label="Measured Tide")
171
+ plot_resid = gr.Plot(label="Residuals")
172
+ with gr.Row():
173
+ plot_pred = gr.Plot(label="Predicted Tide")
174
+ status_output = gr.Textbox(label="Status / Error", interactive=False, lines=1)
175
+
176
+ run_btn.click(fn=tide_analysis_for_month_gradio,
177
+ inputs=month_input,
178
+ outputs=[status_output, plot_pred, plot_meas, plot_resid])
179
+
180
+
181
+ if __name__ == "__main__":
182
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==24.1.0
2
+ aiohappyeyeballs==2.6.1
3
+ aiohttp==3.12.15
4
+ aiosignal==1.4.0
5
+ annotated-types==0.7.0
6
+ anyio==4.10.0
7
+ appnope==0.1.4
8
+ asttokens==3.0.0
9
+ async-timeout==5.0.1
10
+ attrs==25.3.0
11
+ backcall==0.2.0
12
+ beautifulsoup4==4.13.4
13
+ bleach==6.2.0
14
+ Brotli==1.1.0
15
+ cachetools==6.1.0
16
+ certifi==2025.8.3
17
+ charset-normalizer==3.4.2
18
+ click==8.2.1
19
+ decorator==5.2.1
20
+ defusedxml==0.7.1
21
+ docopt==0.6.2
22
+ exceptiongroup==1.3.0
23
+ executing==2.2.0
24
+ fastapi==0.116.1
25
+ fastjsonschema==2.21.1
26
+ ffmpy==0.6.1
27
+ filelock==3.18.0
28
+ frozenlist==1.7.0
29
+ fsspec==2025.7.0
30
+ gradio==5.40.0
31
+ gradio_client==1.11.0
32
+ groovy==0.1.2
33
+ h11==0.16.0
34
+ hf-xet==1.1.5
35
+ httpcore==1.0.9
36
+ httpx==0.28.1
37
+ huggingface-hub==0.34.3
38
+ idna==3.10
39
+ ipython==8.12.3
40
+ jedi==0.19.2
41
+ Jinja2==3.1.6
42
+ jsonschema==4.25.0
43
+ jsonschema-specifications==2025.4.1
44
+ jupyter_client==8.6.3
45
+ jupyter_core==5.8.1
46
+ jupyterlab_pygments==0.3.0
47
+ markdown-it-py==3.0.0
48
+ MarkupSafe==3.0.2
49
+ matplotlib-inline==0.1.7
50
+ mdurl==0.1.2
51
+ mistune==3.1.3
52
+ multidict==6.6.3
53
+ narwhals==2.0.1
54
+ nbclient==0.10.2
55
+ nbconvert==7.16.6
56
+ nbformat==5.10.4
57
+ numpy==2.2.6
58
+ orjson==3.11.1
59
+ packaging==25.0
60
+ pandas==2.3.1
61
+ pandocfilters==1.5.1
62
+ parso==0.8.4
63
+ pexpect==4.9.0
64
+ pickleshare==0.7.5
65
+ pillow==11.3.0
66
+ pipreqs==0.5.0
67
+ platformdirs==4.3.8
68
+ plotly==6.2.0
69
+ prompt_toolkit==3.0.51
70
+ propcache==0.3.2
71
+ ptyprocess==0.7.0
72
+ pure_eval==0.2.3
73
+ pydantic==2.11.7
74
+ pydantic_core==2.33.2
75
+ pydub==0.25.1
76
+ Pygments==2.19.2
77
+ python-dateutil==2.9.0.post0
78
+ python-multipart==0.0.20
79
+ pytz==2025.2
80
+ PyYAML==6.0.2
81
+ pyzmq==27.0.1
82
+ referencing==0.36.2
83
+ requests==2.32.4
84
+ rich==14.1.0
85
+ rpds-py==0.26.0
86
+ ruff==0.12.7
87
+ safehttpx==0.1.6
88
+ semantic-version==2.10.0
89
+ shellingham==1.5.4
90
+ six==1.17.0
91
+ sniffio==1.3.1
92
+ soupsieve==2.7
93
+ stack-data==0.6.3
94
+ starlette==0.47.2
95
+ tinycss2==1.4.0
96
+ tomlkit==0.13.3
97
+ tornado==6.5.1
98
+ tqdm==4.67.1
99
+ traitlets==5.14.3
100
+ typer==0.16.0
101
+ typing-inspection==0.4.1
102
+ typing_extensions==4.14.1
103
+ tzdata==2025.2
104
+ urllib3==2.5.0
105
+ uvicorn==0.35.0
106
+ wcwidth==0.2.13
107
+ webencodings==0.5.1
108
+ websockets==15.0.1
109
+ yarg==0.1.9
110
+ yarl==1.20.1