alwaysgood commited on
Commit
df21c71
·
verified ·
1 Parent(s): 7646174

Update api_utils.py

Browse files
Files changed (1) hide show
  1. api_utils.py +85 -169
api_utils.py CHANGED
@@ -3,204 +3,120 @@ import pandas as pd
3
  import pytz
4
  import plotly.graph_objects as go
5
  from plotly.subplots import make_subplots
6
-
7
  from supabase_utils import get_supabase_client
8
  from config import STATION_NAMES
9
 
10
- def api_get_current_tide(station_id):
11
- """현재 조위 조회"""
12
  supabase = get_supabase_client()
13
  if not supabase:
14
- return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
15
 
16
  try:
17
- result = supabase.table('tide_predictions') \
18
- .select('predicted_at, final_tide_level') \
19
  .eq('station_id', station_id) \
20
- .order('predicted_at', desc=True) \
21
- .limit(1) \
 
22
  .execute()
23
-
24
- if result.data:
25
- return result.data[0]
26
- else:
27
- return {"error": "데이터가 없습니다."}
28
- except Exception as e:
29
- return {"error": f"데이터 조회 오류: {e}"}
30
-
31
- def api_get_historical_tide(station_id, date_str, hours=24):
32
- """과거 특정 날짜의 조위 데이터 조회"""
33
- supabase = get_supabase_client()
34
- if not supabase:
35
- return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
36
-
37
- try:
38
- start_time = datetime.strptime(date_str, '%Y-%m-%d')
39
- start_time_kst = pytz.timezone('Asia/Seoul').localize(start_time)
40
- end_time_kst = start_time_kst + timedelta(hours=hours)
41
-
42
- start_utc = start_time_kst.astimezone(pytz.UTC).isoformat()
43
- end_utc = end_time_kst.astimezone(pytz.UTC).isoformat()
44
-
45
- result = supabase.table('historical_tide') \
46
- .select('observed_at, tide_level') \
47
- .eq('station_id', station_id) \
48
- .gte('observed_at', start_utc) \
49
- .lte('observed_at', end_utc) \
50
- .order('observed_at') \
51
- .execute()
52
-
53
  if not result.data:
54
- return {"error": f"{date_str} 대한 과거 데이터가 없습니다."}
55
-
56
- df = pd.DataFrame(result.data)
57
- df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
58
- df['tide_level'] = pd.to_numeric(df['tide_level'])
59
-
60
- fig = go.Figure()
61
- fig.add_trace(go.Scatter(x=df['observed_at'], y=df['tide_level'], mode='lines',
62
- name=f'{STATION_NAMES.get(station_id, station_id)} 조위'))
63
- fig.update_layout(
64
- title=f'{STATION_NAMES.get(station_id, station_id)} - {date_str} 조위',
65
- xaxis_title='시간',
66
- yaxis_title='조위 (cm)',
67
- height=400
68
- )
69
- return fig
70
-
71
  except Exception as e:
72
- return {"error": f"데이터 조회 오류 발생: {e}"}
73
 
74
- def api_get_historical_extremes(station_id, date_str):
75
- """과거 특정 날짜의 만조/간조 정보"""
76
- supabase = get_supabase_client()
77
- if not supabase:
78
- return {"error": "Supabase 클라이언트를 생성할 없습니다."}
 
79
 
80
- try:
81
- start_time = datetime.strptime(date_str, '%Y-%m-%d')
82
- start_time_kst = pytz.timezone('Asia/Seoul').localize(start_time)
83
- end_time_kst = start_time_kst + timedelta(days=1)
84
 
85
- start_utc = start_time_kst.astimezone(pytz.UTC).isoformat()
86
- end_utc = end_time_kst.astimezone(pytz.UTC).isoformat()
87
-
88
- result = supabase.table('historical_tide') \
89
- .select('observed_at, tide_level') \
90
- .eq('station_id', station_id) \
91
- .gte('observed_at', start_utc) \
92
- .lte('observed_at', end_utc) \
93
- .order('observed_at') \
94
- .execute()
95
-
96
- if not result.data or len(result.data) < 3:
97
- return {"error": f"{date_str}의 만조/간조를 계산할 데이터가 부족합니다."}
98
-
99
- df = pd.DataFrame(result.data)
100
- df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
101
- df['tide_level'] = pd.to_numeric(df['tide_level'])
102
 
 
 
 
103
  df['min'] = df.tide_level[(df.tide_level.shift(1) > df.tide_level) & (df.tide_level.shift(-1) > df.tide_level)]
104
  df['max'] = df.tide_level[(df.tide_level.shift(1) < df.tide_level) & (df.tide_level.shift(-1) < df.tide_level)]
105
-
106
  extremes_df = df.dropna(subset=['min', 'max'], how='all').copy()
107
- extremes_df['type'] = extremes_df.apply(lambda row: '만조' if pd.notna(row['max']) else '간조', axis=1)
108
  extremes_df['value'] = extremes_df.apply(lambda row: row['max'] if pd.notna(row['max']) else row['min'], axis=1)
109
  extremes_df['time'] = extremes_df['observed_at'].dt.strftime('%H:%M')
 
110
 
111
- return extremes_df[['time', 'type', 'value']]
 
 
 
 
 
 
 
 
112
 
113
- except Exception as e:
114
- return {"error": f"데이터 처리 중 오류 발생: {e}"}
115
 
116
- def api_compare_dates(station_id, date1, date2):
117
  """두 날짜의 조위 패턴 비교"""
118
- supabase = get_supabase_client()
119
- if not supabase:
120
- return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
 
121
 
122
- def get_data_for_date(target_date):
123
- start = pytz.timezone('Asia/Seoul').localize(datetime.strptime(target_date, '%Y-%m-%d'))
124
- end = start + timedelta(days=1)
125
- res = supabase.table('historical_tide') \
126
- .select('observed_at, tide_level') \
127
- .eq('station_id', station_id) \
128
- .gte('observed_at', start.astimezone(pytz.UTC).isoformat()) \
129
- .lte('observed_at', end.astimezone(pytz.UTC).isoformat()) \
130
- .order('observed_at') \
131
- .execute()
132
- return res.data
133
 
134
- data1 = get_data_for_date(date1)
135
- data2 = get_data_for_date(date2)
136
-
137
- if not data1 or not data2:
138
- return {"error": "두 날짜 중 하나의 데이터가 없습니다."}
139
-
140
- df1 = pd.DataFrame(data1)
141
- df1['tide_level'] = pd.to_numeric(df1['tide_level'])
142
  df1['minutes_from_start'] = (pd.to_datetime(df1['observed_at']) - pd.to_datetime(df1['observed_at']).iloc[0]).dt.total_seconds() / 60
143
-
144
- df2 = pd.DataFrame(data2)
145
- df2['tide_level'] = pd.to_numeric(df2['tide_level'])
146
  df2['minutes_from_start'] = (pd.to_datetime(df2['observed_at']) - pd.to_datetime(df2['observed_at']).iloc[0]).dt.total_seconds() / 60
147
 
148
  fig = go.Figure()
149
  fig.add_trace(go.Scatter(x=df1['minutes_from_start'], y=df1['tide_level'], mode='lines', name=date1))
150
  fig.add_trace(go.Scatter(x=df2['minutes_from_start'], y=df2['tide_level'], mode='lines', name=date2))
151
- fig.update_layout(title=f'{STATION_NAMES.get(station_id, station_id)} 조위 비교: {date1} vs {date2}',
152
- xaxis_title='자정부터 경과 시간(분)', yaxis_title='조위 (cm)')
153
- return fig
154
-
155
- def api_get_monthly_summary(station_id, year, month):
156
- """월간 조위 요약 통계"""
157
- supabase = get_supabase_client()
158
- if not supabase:
159
- return {"error": "Supabase 클라이언트를 생성할 수 없습니다."}
160
-
161
- try:
162
- start_date = f"{year}-{int(month):02d}-01"
163
- end_date = (datetime.strptime(start_date, '%Y-%m-%d') + pd.offsets.MonthEnd(1)).strftime('%Y-%m-%d')
164
-
165
- start_utc = pytz.timezone('Asia/Seoul').localize(datetime.strptime(start_date, '%Y-%m-%d')).astimezone(pytz.UTC).isoformat()
166
- end_utc = (pytz.timezone('Asia/Seoul').localize(datetime.strptime(end_date, '%Y-%m-%d')) + timedelta(days=1)).astimezone(pytz.UTC).isoformat()
167
-
168
- result = supabase.table('historical_tide') \
169
- .select('observed_at, tide_level') \
170
- .eq('station_id', station_id) \
171
- .gte('observed_at', start_utc) \
172
- .lte('observed_at', end_utc) \
173
- .order('observed_at') \
174
- .execute()
175
-
176
- if not result.data:
177
- return {"error": f"{year} {month}월 데이터가 없습니다."}
178
-
179
- df = pd.DataFrame(result.data)
180
- df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
181
- df['tide_level'] = pd.to_numeric(df['tide_level'])
182
-
183
- highest = df.loc[df['tide_level'].idxmax()]
184
- lowest = df.loc[df['tide_level'].idxmin()]
185
- avg_tide = df['tide_level'].mean()
186
-
187
- df['date'] = df['observed_at'].dt.date
188
- daily_range = df.groupby('date')['tide_level'].apply(lambda x: x.max() - x.min())
189
- avg_range = daily_range.mean()
190
-
191
- summary = {
192
- "최고 조위": f"{highest['tide_level']:.1f}cm ({highest['observed_at'].strftime('%Y-%m-%d %H:%M')})",
193
- "최저 조위": f"{lowest['tide_level']:.1f}cm ({lowest['observed_at'].strftime('%Y-%m-%d %H:%M')})",
194
- "평균 조위": f"{avg_tide:.1f}cm",
195
- "평균 조차": f"{avg_range:.1f}cm"
196
- }
197
-
198
- fig = make_subplots(rows=2, cols=1, subplot_titles=("일별 조위 변화", "일별 조차"))
199
- fig.add_trace(go.Box(x=df['observed_at'].dt.strftime('%Y-%m-%d'), y=df['tide_level'], name='조위'), row=1, col=1)
200
- fig.add_trace(go.Bar(x=daily_range.index.strftime('%Y-%m-%d'), y=daily_range.values, name='조차'), row=2, col=1)
201
- fig.update_layout(height=700, title_text=f"{STATION_NAMES.get(station_id, station_id)} - {year}년 {month}월 요약")
202
-
203
- return summary, fig
204
-
205
- except Exception as e:
206
- return {"error": f"월간 요약 생성 중 오류 발생: {e}"}, None
 
3
  import pytz
4
  import plotly.graph_objects as go
5
  from plotly.subplots import make_subplots
 
6
  from supabase_utils import get_supabase_client
7
  from config import STATION_NAMES
8
 
9
+ def fetch_tide_data(station_id, start_utc, end_utc, table='historical_tide'):
10
+ """공통 데이터 조회 함수"""
11
  supabase = get_supabase_client()
12
  if not supabase:
13
+ raise ValueError("Supabase 클라이언트를 생성할 수 없습니다.")
14
 
15
  try:
16
+ result = supabase.table(table) \
17
+ .select('observed_at, tide_level' if table == 'historical_tide' else 'predicted_at, final_tide_level') \
18
  .eq('station_id', station_id) \
19
+ .gte('observed_at' if table == 'historical_tide' else 'predicted_at', start_utc) \
20
+ .lte('observed_at' if table == 'historical_tide' else 'predicted_at', end_utc) \
21
+ .order('observed_at' if table == 'historical_tide' else 'predicted_at') \
22
  .execute()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  if not result.data:
24
+ raise ValueError(f"데이터가 없습니다: {station_id}, {start_utc} ~ {end_utc}")
25
+ return pd.DataFrame(result.data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  except Exception as e:
27
+ raise ValueError(f"데이터 조회 오류: {e}")
28
 
29
+ def get_tide_data(station_id, start_date=None, end_date=None, include_extremes=False, return_plot=False):
30
+ """조위 데이터 조회 시각화"""
31
+ start_date = start_date or datetime.now(pytz.timezone('Asia/Seoul')).strftime('%Y-%m-%d')
32
+ start_time = pytz.timezone('Asia/Seoul').localize(datetime.strptime(start_date, '%Y-%m-%d'))
33
+ end_time = start_time + timedelta(hours=24) if not end_date else \
34
+ pytz.timezone('Asia/Seoul').localize(datetime.strptime(end_date, '%Y-%m-%d')) + timedelta(hours=24)
35
 
36
+ start_utc = start_time.astimezone(pytz.UTC).isoformat()
37
+ end_utc = end_time.astimezone(pytz.UTC).isoformat()
 
 
38
 
39
+ df = fetch_tide_data(station_id, start_utc, end_utc)
40
+ df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
41
+ df['tide_level'] = pd.to_numeric(df['tide_level'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ result = {"data": df}
44
+
45
+ if include_extremes:
46
  df['min'] = df.tide_level[(df.tide_level.shift(1) > df.tide_level) & (df.tide_level.shift(-1) > df.tide_level)]
47
  df['max'] = df.tide_level[(df.tide_level.shift(1) < df.tide_level) & (df.tide_level.shift(-1) < df.tide_level)]
 
48
  extremes_df = df.dropna(subset=['min', 'max'], how='all').copy()
49
+ extremes_df['type'] = extremes_df.apply(lambda row: 'High Tide' if pd.notna(row['max']) else 'Low Tide', axis=1)
50
  extremes_df['value'] = extremes_df.apply(lambda row: row['max'] if pd.notna(row['max']) else row['min'], axis=1)
51
  extremes_df['time'] = extremes_df['observed_at'].dt.strftime('%H:%M')
52
+ result["extremes"] = extremes_df[['time', 'type', 'value']]
53
 
54
+ if return_plot:
55
+ fig = go.Figure()
56
+ fig.add_trace(go.Scatter(x=df['observed_at'], y=df['tide_level'], mode='lines',
57
+ name=f'{STATION_NAMES.get(station_id, station_id)} Tide'))
58
+ fig.update_layout(
59
+ title=f'{STATION_NAMES.get(station_id, station_id)} Tide: {start_date} to {end_date or start_date}',
60
+ xaxis_title='Time', yaxis_title='Tide Level (cm)', height=400
61
+ )
62
+ result["plot"] = fig
63
 
64
+ return result
 
65
 
66
+ def compare_tide_patterns(station_id, date1, date2, time_window=24):
67
  """두 날짜의 조위 패턴 비교"""
68
+ start1 = pytz.timezone('Asia/Seoul').localize(datetime.strptime(date1, '%Y-%m-%d'))
69
+ start2 = pytz.timezone('Asia/Seoul').localize(datetime.strptime(date2, '%Y-%m-%d'))
70
+ end1 = start1 + timedelta(hours=time_window)
71
+ end2 = start2 + timedelta(hours=time_window)
72
 
73
+ df1 = fetch_tide_data(station_id, start1.astimezone(pytz.UTC).isoformat(), end1.astimezone(pytz.UTC).isoformat())
74
+ df2 = fetch_tide_data(station_id, start2.astimezone(pytz.UTC).isoformat(), end2.astimezone(pytz.UTC).isoformat())
 
 
 
 
 
 
 
 
 
75
 
 
 
 
 
 
 
 
 
76
  df1['minutes_from_start'] = (pd.to_datetime(df1['observed_at']) - pd.to_datetime(df1['observed_at']).iloc[0]).dt.total_seconds() / 60
 
 
 
77
  df2['minutes_from_start'] = (pd.to_datetime(df2['observed_at']) - pd.to_datetime(df2['observed_at']).iloc[0]).dt.total_seconds() / 60
78
 
79
  fig = go.Figure()
80
  fig.add_trace(go.Scatter(x=df1['minutes_from_start'], y=df1['tide_level'], mode='lines', name=date1))
81
  fig.add_trace(go.Scatter(x=df2['minutes_from_start'], y=df2['tide_level'], mode='lines', name=date2))
82
+ fig.update_layout(
83
+ title=f'{STATION_NAMES.get(station_id, station_id)} Tide Comparison: {date1} vs {date2}',
84
+ xaxis_title='Minutes from Midnight', yaxis_title='Tide Level (cm)', height=400
85
+ )
86
+ return {"data": [df1, df2], "plot": fig}
87
+
88
+ def get_tide_summary(station_id, year, month, summary_type='monthly'):
89
+ """월간/연간 조위 요약"""
90
+ start_date = f"{year}-{int(month):02d}-01"
91
+ end_date = (datetime.strptime(start_date, '%Y-%m-%d') + pd.offsets.MonthEnd(1)).strftime('%Y-%m-%d')
92
+
93
+ df = fetch_tide_data(station_id,
94
+ pytz.timezone('Asia/Seoul').localize(datetime.strptime(start_date, '%Y-%m-%d')).astimezone(pytz.UTC).isoformat(),
95
+ (pytz.timezone('Asia/Seoul').localize(datetime.strptime(end_date, '%Y-%m-%d')) + timedelta(days=1)).astimezone(pytz.UTC).isoformat())
96
+
97
+ df['observed_at'] = pd.to_datetime(df['observed_at']).dt.tz_convert('Asia/Seoul')
98
+ df['tide_level'] = pd.to_numeric(df['tide_level'])
99
+
100
+ highest = df.loc[df['tide_level'].idxmax()]
101
+ lowest = df.loc[df['tide_level'].idxmin()]
102
+ avg_tide = df['tide_level'].mean()
103
+ df['date'] = df['observed_at'].dt.date
104
+ daily_range = df.groupby('date')['tide_level'].apply(lambda x: x.max() - x.min())
105
+ avg_range = daily_range.mean()
106
+
107
+ summary = {
108
+ "Highest Tide": f"{highest['tide_level']:.1f}cm ({highest['observed_at'].strftime('%Y-%m-%d %H:%M')})",
109
+ "Lowest Tide": f"{lowest['tide_level']:.1f}cm ({lowest['observed_at'].strftime('%Y-%m-%d %H:%M')})",
110
+ "Average Tide": f"{avg_tide:.1f}cm",
111
+ "Average Range": f"{avg_range:.1f}cm"
112
+ }
113
+
114
+ fig = make_subplots(rows=2, cols=1, subplot_titles=("Daily Tide Variation", "Daily Tide Range"))
115
+ fig.add_trace(go.Box(x=df['observed_at'].dt.strftime('%Y-%m-%d'), y=df['tide_level'], name='Tide'), row=1, col=1)
116
+ fig.add_trace(go.Bar(x=daily_range.index, y=daily_range.values, name='Range'), row=2, col=1)
117
+ fig.update_layout(
118
+ height=700,
119
+ title_text=f"{STATION_NAMES.get(station_id, station_id)} - {year} {month} Summary"
120
+ )
121
+
122
+ return {"summary": summary, "plot": fig}