enacimie commited on
Commit
b68d587
·
verified ·
1 Parent(s): 93b5700

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +346 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,348 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import plotly.express as px
5
+ import plotly.graph_objects as go
6
+ from statsmodels.tsa.seasonal import seasonal_decompose
7
+ from statsmodels.tsa.stattools import adfuller, acf, pacf
8
+ from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
9
+ from statsmodels.tsa.holtwinters import ExponentialSmoothing
10
+ from statsmodels.tsa.arima.model import ARIMA
11
+ from prophet import Prophet
12
+ from sklearn.metrics import mean_absolute_error, mean_squared_error
13
+ import matplotlib.pyplot as plt
14
+ import io
15
+ import warnings
16
+ warnings.filterwarnings("ignore")
17
+
18
+ # Metadata
19
+ AUTHOR = "Eduardo Nacimiento García"
20
+ EMAIL = "enacimie@ull.edu.es"
21
+ LICENSE = "Apache 2.0"
22
+
23
+ # Page config
24
+ st.set_page_config(
25
+ page_title="SimpleTS",
26
+ page_icon="📈",
27
+ layout="wide",
28
+ initial_sidebar_state="expanded",
29
+ )
30
+
31
+ # Title
32
+ st.title("📈 SimpleTS")
33
+ st.markdown(f"**Author:** {AUTHOR} | **Email:** {EMAIL} | **License:** {LICENSE}")
34
+ st.write("""
35
+ Upload a time series CSV or use the demo dataset to visualize, analyze, and forecast your data.
36
+ """)
37
+
38
+ # === GENERATE DEMO TIME SERIES ===
39
+ @st.cache_data
40
+ def create_demo_ts(freq='D', periods=365):
41
+ np.random.seed(42)
42
+ date_rng = pd.date_range(start='2023-01-01', periods=periods, freq=freq)
43
+ # Create trend + seasonality + noise
44
+ trend = np.linspace(100, 200, periods)
45
+ if freq in ['D', 'W']:
46
+ seasonality = 20 * np.sin(2 * np.pi * np.arange(periods) / 365.25)
47
+ elif freq == 'M':
48
+ seasonality = 25 * np.sin(2 * np.pi * np.arange(periods) / 12)
49
+ noise = np.random.normal(0, 5, periods)
50
+ values = trend + seasonality + noise
51
+ df = pd.DataFrame({
52
+ 'Date': date_rng,
53
+ 'Value': values
54
+ })
55
+ return df
56
+
57
+ # === LOAD DATA ===
58
+ if "demo_loaded" not in st.session_state:
59
+ st.session_state.demo_loaded = False
60
+ st.session_state.freq = 'D'
61
+
62
+ col1, col2, col3 = st.columns(3)
63
+ with col1:
64
+ if st.button("🧪 Load Daily Demo"):
65
+ st.session_state.demo_loaded = True
66
+ st.session_state.freq = 'D'
67
+ st.session_state.df = create_demo_ts('D', 365)
68
+ st.success("✅ Daily demo loaded!")
69
+ with col2:
70
+ if st.button("🧪 Load Monthly Demo"):
71
+ st.session_state.demo_loaded = True
72
+ st.session_state.freq = 'M'
73
+ st.session_state.df = create_demo_ts('M', 48)
74
+ st.success("✅ Monthly demo loaded!")
75
+ with col3:
76
+ if st.button("🧪 Load Weekly Demo"):
77
+ st.session_state.demo_loaded = True
78
+ st.session_state.freq = 'W'
79
+ st.session_state.df = create_demo_ts('W', 104)
80
+ st.success("✅ Weekly demo loaded!")
81
+
82
+ uploaded_file = st.file_uploader("📂 Upload your time series CSV (must have a date and a value column)", type=["csv"])
83
+
84
+ # Use demo or uploaded file
85
+ if uploaded_file:
86
+ df = pd.read_csv(uploaded_file)
87
+ st.session_state.df = df
88
+ st.session_state.demo_loaded = False
89
+ st.success("✅ File uploaded successfully.")
90
+ elif "df" in st.session_state:
91
+ df = st.session_state.df
92
+ freq = st.session_state.freq
93
+ if st.session_state.demo_loaded:
94
+ st.info(f"Using **{freq}** frequency demo dataset.")
95
+ else:
96
+ df = None
97
+ st.info("👆 Upload a CSV or load a demo dataset to begin.")
98
+ st.stop()
99
+
100
+ # Show data preview
101
+ with st.expander("🔍 Data Preview (first 10 rows)"):
102
+ st.dataframe(df.head(10))
103
+
104
+ # === SELECT DATE AND VALUE COLUMNS ===
105
+ st.subheader("📅 Configure Time Series")
106
+
107
+ date_col = st.selectbox("Select date column:", df.columns)
108
+ value_col = st.selectbox("Select value column:", [col for col in df.columns if col != date_col])
109
+
110
+ # Convert to datetime and set index
111
+ try:
112
+ df[date_col] = pd.to_datetime(df[date_col])
113
+ df = df.set_index(date_col).sort_index()
114
+ ts = df[value_col]
115
+ st.success("✅ Time series configured successfully.")
116
+ except Exception as e:
117
+ st.error(f"❌ Error processing date column: {e}")
118
+ st.stop()
119
+
120
+ # Plot original series
121
+ st.subheader("📊 Original Time Series")
122
+ fig = px.line(x=ts.index, y=ts.values, labels={'x': 'Date', 'y': value_col}, title="Original Time Series")
123
+ st.plotly_chart(fig, use_container_width=True)
124
+
125
+ # === TIME SERIES ANALYSIS ===
126
+ st.header("🔬 Time Series Analysis")
127
+
128
+ # Stationarity test (ADF)
129
+ st.subheader("📉 Stationarity Test (ADF)")
130
+ adf_result = adfuller(ts.dropna())
131
+ st.write(f"- **ADF Statistic:** {adf_result[0]:.4f}")
132
+ st.write(f"- **p-value:** {adf_result[1]:.4f}")
133
+ if adf_result[1] < 0.05:
134
+ st.success("🟢 Series is stationary (p < 0.05)")
135
+ else:
136
+ st.warning("🟠 Series is non-stationary (p >= 0.05) — consider differencing")
137
+
138
+ # Seasonal Decomposition
139
+ st.subheader("🎯 Seasonal Decomposition")
140
+ period_options = {
141
+ 'D': 365,
142
+ 'W': 52,
143
+ 'M': 12,
144
+ 'Q': 4,
145
+ 'Y': 1
146
+ }
147
+ freq = st.session_state.freq if st.session_state.demo_loaded else 'D'
148
+ default_period = period_options.get(freq, 12)
149
+
150
+ period = st.number_input("Seasonal period (e.g., 12 for monthly, 365 for daily):",
151
+ min_value=2, value=default_period, step=1)
152
+
153
+ try:
154
+ decomposition = seasonal_decompose(ts.dropna(), model='additive', period=int(period), extrapolate_trend='freq')
155
+
156
+ # Plot decomposition
157
+ fig = go.Figure()
158
+ fig.add_trace(go.Scatter(x=decomposition.observed.index, y=decomposition.observed, mode='lines', name='Observed'))
159
+ fig.add_trace(go.Scatter(x=decomposition.trend.index, y=decomposition.trend, mode='lines', name='Trend'))
160
+ fig.add_trace(go.Scatter(x=decomposition.seasonal.index, y=decomposition.seasonal, mode='lines', name='Seasonal'))
161
+ fig.add_trace(go.Scatter(x=decomposition.resid.index, y=decomposition.resid, mode='lines', name='Residual'))
162
+ fig.update_layout(title="Seasonal Decomposition", height=600)
163
+ st.plotly_chart(fig, use_container_width=True)
164
+ except Exception as e:
165
+ st.error(f"Could not decompose series: {e}")
166
+
167
+ # ACF / PACF Plots
168
+ st.subheader("🔗 Autocorrelation (ACF) & Partial Autocorrelation (PACF)")
169
+
170
+ max_lags = st.slider("Max lags:", min_value=10, max_value=100, value=40, step=5)
171
+
172
+ col1, col2 = st.columns(2)
173
+
174
+ with col1:
175
+ st.write("**ACF Plot**")
176
+ fig_acf, ax_acf = plt.subplots(figsize=(6, 4))
177
+ plot_acf(ts.dropna(), lags=max_lags, ax=ax_acf)
178
+ st.pyplot(fig_acf)
179
+
180
+ with col2:
181
+ st.write("**PACF Plot**")
182
+ fig_pacf, ax_pacf = plt.subplots(figsize=(6, 4))
183
+ plot_pacf(ts.dropna(), lags=max_lags, ax=ax_pacf)
184
+ st.pyplot(fig_pacf)
185
+
186
+ # === FORECASTING MODELS ===
187
+ st.header("🤖 Forecasting Models")
188
+
189
+ # Train/test split
190
+ test_size = st.slider("Test set size (as % of data):", min_value=5, max_value=40, value=20, step=5)
191
+ split_point = int(len(ts) * (1 - test_size/100))
192
+ train, test = ts[:split_point], ts[split_point:]
193
+
194
+ st.write(f"Training on {len(train)} points, testing on {len(test)} points.")
195
+
196
+ model_choice = st.selectbox("Choose forecasting model:",
197
+ ["Holt-Winters Exponential Smoothing", "ARIMA", "Prophet"])
198
+
199
+ # Initialize forecast variable
200
+ forecast = None
201
+ model = None
202
+
203
+ if model_choice == "Holt-Winters Exponential Smoothing":
204
+ seasonal_periods = st.number_input("Seasonal periods:", min_value=2, value=period, step=1)
205
+ try:
206
+ hw_model = ExponentialSmoothing(
207
+ train,
208
+ trend='add',
209
+ seasonal='add',
210
+ seasonal_periods=seasonal_periods
211
+ ).fit()
212
+ forecast = hw_model.forecast(len(test))
213
+ model = hw_model
214
+ except Exception as e:
215
+ st.error(f"Could not fit Holt-Winters model: {e}")
216
+
217
+ elif model_choice == "ARIMA":
218
+ col1, col2, col3 = st.columns(3)
219
+ p = col1.number_input("AR order (p):", min_value=0, max_value=5, value=1)
220
+ d = col2.number_input("Differencing order (d):", min_value=0, max_value=2, value=1)
221
+ q = col3.number_input("MA order (q):", min_value=0, max_value=5, value=1)
222
+ try:
223
+ arima_model = ARIMA(train, order=(p, d, q)).fit()
224
+ forecast = arima_model.forecast(len(test))
225
+ model = arima_model
226
+ except Exception as e:
227
+ st.error(f"Could not fit ARIMA model: {e}")
228
+
229
+ elif model_choice == "Prophet":
230
+ # Prepare data for Prophet
231
+ prophet_df = pd.DataFrame({
232
+ 'ds': train.index,
233
+ 'y': train.values
234
+ })
235
+ try:
236
+ prophet_model = Prophet(
237
+ yearly_seasonality=True if freq in ['D', 'W'] else False,
238
+ weekly_seasonality=True if freq == 'D' else False,
239
+ daily_seasonality=False
240
+ )
241
+ if freq == 'M':
242
+ prophet_model.add_seasonality(name='monthly', period=30.5, fourier_order=5)
243
+ prophet_model.fit(prophet_df)
244
+
245
+ # Forecast
246
+ future = pd.DataFrame({'ds': test.index})
247
+ forecast_df = prophet_model.predict(future)
248
+ forecast = forecast_df['yhat'].values
249
+ model = prophet_model
250
+ except Exception as e:
251
+ st.error(f"Could not fit Prophet model: {e}")
252
+
253
+ # Show results if forecast exists
254
+ if forecast is not None:
255
+ # Metrics
256
+ mae = mean_absolute_error(test, forecast)
257
+ mse = mean_squared_error(test, forecast)
258
+ rmse = np.sqrt(mse)
259
+
260
+ st.subheader("📈 Forecast Results")
261
+ col1, col2, col3 = st.columns(3)
262
+ col1.metric("MAE", f"{mae:.2f}")
263
+ col2.metric("MSE", f"{mse:.2f}")
264
+ col3.metric("RMSE", f"{rmse:.2f}")
265
+
266
+ # Plot forecast vs actual
267
+ fig = go.Figure()
268
+ fig.add_trace(go.Scatter(x=train.index, y=train, mode='lines', name='Training', line=dict(color='blue')))
269
+ fig.add_trace(go.Scatter(x=test.index, y=test, mode='lines', name='Actual', line=dict(color='green')))
270
+ fig.add_trace(go.Scatter(x=test.index, y=forecast, mode='lines+markers', name='Forecast', line=dict(color='red', dash='dash')))
271
+ fig.update_layout(
272
+ title=f"{model_choice} Forecast",
273
+ xaxis_title="Date",
274
+ yaxis_title=value_col,
275
+ legend=dict(x=0, y=1)
276
+ )
277
+ st.plotly_chart(fig, use_container_width=True)
278
+
279
+ # Allow forecasting into future
280
+ st.subheader("🔮 Forecast Future Periods")
281
+ future_periods = st.number_input("Number of future periods to forecast:", min_value=1, max_value=365, value=30, step=1)
282
+
283
+ if st.button("🚀 Generate Future Forecast"):
284
+ try:
285
+ if model_choice == "Holt-Winters Exponential Smoothing":
286
+ future_forecast = model.forecast(future_periods)
287
+ last_date = ts.index[-1]
288
+ if freq == 'D':
289
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=future_periods, freq='D')
290
+ elif freq == 'W':
291
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(weeks=1), periods=future_periods, freq='W')
292
+ elif freq == 'M':
293
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=future_periods, freq='M')
294
+ else:
295
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=future_periods, freq='D')
296
+
297
+ elif model_choice == "ARIMA":
298
+ future_forecast = model.forecast(future_periods)
299
+ last_date = ts.index[-1]
300
+ if freq == 'D':
301
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=future_periods, freq='D')
302
+ elif freq == 'W':
303
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(weeks=1), periods=future_periods, freq='W')
304
+ elif freq == 'M':
305
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=future_periods, freq='M')
306
+ else:
307
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=future_periods, freq='D')
308
+
309
+ elif model_choice == "Prophet":
310
+ last_date = ts.index[-1]
311
+ if freq == 'D':
312
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=future_periods, freq='D')
313
+ elif freq == 'W':
314
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(weeks=1), periods=future_periods, freq='W')
315
+ elif freq == 'M':
316
+ future_dates = pd.date_range(start=last_date + pd.DateOffset(months=1), periods=future_periods, freq='M')
317
+ else:
318
+ future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=future_periods, freq='D')
319
+
320
+ future_df = pd.DataFrame({'ds': future_dates})
321
+ forecast_df = model.predict(future_df)
322
+ future_forecast = forecast_df['yhat'].values
323
+
324
+ # Plot future forecast
325
+ fig_future = go.Figure()
326
+ fig_future.add_trace(go.Scatter(x=ts.index, y=ts.values, mode='lines', name='Historical', line=dict(color='blue')))
327
+ fig_future.add_trace(go.Scatter(x=future_dates, y=future_forecast, mode='lines+markers', name='Future Forecast', line=dict(color='red', dash='dash')))
328
+ fig_future.update_layout(
329
+ title="Future Forecast",
330
+ xaxis_title="Date",
331
+ yaxis_title=value_col
332
+ )
333
+ st.plotly_chart(fig_future, use_container_width=True)
334
+
335
+ # Show as table
336
+ forecast_df = pd.DataFrame({
337
+ 'Date': future_dates,
338
+ 'Forecast': future_forecast
339
+ })
340
+ with st.expander("📋 View Forecast Table"):
341
+ st.dataframe(forecast_df)
342
+
343
+ except Exception as e:
344
+ st.error(f"Could not generate future forecast: {e}")
345
 
346
+ # Footer
347
+ st.markdown("---")
348
+ st.caption(f"© {AUTHOR} | License {LICENSE} | Contact: {EMAIL}")