File size: 11,564 Bytes
e5b6941
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
import gradio as gr
import pandas as pd
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import PolynomialFeatures
from statsmodels.tsa.holtwinters import ExponentialSmoothing
import plotly.graph_objects as go
import tempfile
import os

def clean_date_and_val(df, date_col, val_col):
    df[date_col] = pd.to_datetime(df[date_col], errors='coerce')
    df[val_col] = pd.to_numeric(df[val_col], errors='coerce')
    df = df.dropna(subset=[date_col, val_col]).sort_values(date_col)
    return df

def run_forecast(file_obj, forecast_steps, model_type):
    if file_obj is None:
        return "Please upload a time-series CSV or Excel file.", None, None, None
        
    try:
        if file_obj.name.endswith('.csv'):
            df = pd.read_csv(file_obj.name)
        else:
            df = pd.read_excel(file_obj.name)
    except Exception as e:
        return f"Error reading file: {str(e)}", None, None, None
        
    # Standardize column headers
    date_col, val_col = None, None
    for col in df.columns:
        if col.lower() in ['date', 'year', 'month', 'time', 'timestamp', 'dt']:
            date_col = col
        elif col.lower() in ['value', 'frequency', 'count', 'y', 'sales', 'clicks', 'views']:
            val_col = col
            
    if not date_col or not val_col:
        # Fallbacks
        if len(df.columns) >= 2:
            date_col = df.columns[0]
            val_col = df.columns[1]
        else:
            return "Ensure your file contains at least two columns: Date/Time and Value.", None, None, None
            
    df = clean_date_and_val(df, date_col, val_col)
    
    if len(df) < 5:
        return "Dataset must contain at least 5 clean chronological rows.", None, None, None
        
    dates = df[date_col].tolist()
    values = df[val_col].tolist()
    n = len(values)
    
    # Generate future dates
    try:
        # Try to infer frequency or fallback to simple day offset
        freq = pd.infer_freq(df[date_col])
        if not freq:
            diffs = df[date_col].diff().dropna()
            # Median time delta
            median_delta = diffs.median()
            future_dates = [dates[-1] + (i * median_delta) for i in range(1, forecast_steps + 1)]
        else:
            future_dates = pd.date_range(start=dates[-1], periods=forecast_steps + 1, freq=freq)[1:].tolist()
    except:
        # Absolute fallback: add 1 day offsets
        future_dates = [dates[-1] + pd.Timedelta(days=i) for i in range(1, forecast_steps + 1)]
        
    # Forecasting models
    x_indices = np.arange(n).reshape(-1, 1)
    x_future = np.arange(n, n + forecast_steps).reshape(-1, 1)
    
    forecast_values = []
    lower_bound = []
    upper_bound = []
    
    std_err = np.std(values)  # Base standard error for uncertainty envelopes
    
    if model_type == "Linear Trend":
        model = LinearRegression()
        model.fit(x_indices, values)
        forecast_values = model.predict(x_future)
        
        # Uncertainty grows over time
        for idx, val in enumerate(forecast_values):
            growth = std_err * (1.0 + 0.1 * idx)
            lower_bound.append(val - 1.96 * growth)
            upper_bound.append(val + 1.96 * growth)
            
    elif model_type == "Polynomial Trend (Quadratic)":
        poly = PolynomialFeatures(degree=2)
        x_poly = poly.fit_transform(x_indices)
        x_future_poly = poly.transform(x_future)
        
        model = LinearRegression()
        model.fit(x_poly, values)
        forecast_values = model.predict(x_future_poly)
        
        for idx, val in enumerate(forecast_values):
            growth = std_err * (1.0 + 0.15 * idx)
            lower_bound.append(val - 1.96 * growth)
            upper_bound.append(val + 1.96 * growth)
            
    else:  # Holt-Winters Exponential Smoothing
        try:
            model = ExponentialSmoothing(
                values, 
                trend='add', 
                seasonal=None, 
                damped_trend=True
            )
            fit = model.fit()
            forecast_values = fit.forecast(forecast_steps)
            
            # Simple residuals error calculation for bounds
            resids_std = np.std(fit.resid)
            for idx, val in enumerate(forecast_values):
                growth = resids_std * np.sqrt(1 + idx)  # Error accumulates
                lower_bound.append(val - 1.96 * growth)
                upper_bound.append(val + 1.96 * growth)
        except Exception as e:
            # Fallback to Simple Exponential Smoothing
            try:
                model = ExponentialSmoothing(values, trend=None, seasonal=None)
                fit = model.fit()
                forecast_values = fit.forecast(forecast_steps)
                resids_std = np.std(fit.resid)
                for idx, val in enumerate(forecast_values):
                    growth = resids_std * np.sqrt(1 + idx)
                    lower_bound.append(val - 1.96 * growth)
                    upper_bound.append(val + 1.96 * growth)
            except:
                # Absolute regression fallback
                model = LinearRegression()
                model.fit(x_indices, values)
                forecast_values = model.predict(x_future)
                for idx, val in enumerate(forecast_values):
                    growth = std_err * (1.0 + 0.1 * idx)
                    lower_bound.append(val - 1.96 * growth)
                    upper_bound.append(val + 1.96 * growth)

    # 4. Generate Gorgeous Plotly Chart
    fig = go.Figure()
    
    # Shaded Uncertainty Envelope
    fig.add_trace(go.Scatter(
        x=future_dates + future_dates[::-1],
        y=upper_bound + lower_bound[::-1],
        fill='toself',
        fillcolor='rgba(255, 112, 67, 0.08)',
        line=dict(color='rgba(255,255,255,0)'),
        hoverinfo="skip",
        name="95% Confidence Interval"
    ))
    
    # Historical Actual Line
    fig.add_trace(go.Scatter(
        x=dates,
        y=values,
        mode='lines+markers',
        name='Historical Actuals',
        line=dict(color='#ff7043', width=3),
        marker=dict(size=6)
    ))
    
    # Forecasted Line
    fig.add_trace(go.Scatter(
        x=future_dates,
        y=forecast_values,
        mode='lines+markers',
        name='Projected Forecast',
        line=dict(color='#ffffff', width=2.5, dash='dash'),
        marker=dict(size=6, symbol='diamond')
    ))
    
    fig.update_layout(
        title=f"Time-Series Forecast Trend ({model_type})",
        paper_bgcolor='#16100c',
        plot_bgcolor='#16100c',
        font_color='#f4eee6',
        xaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.05)'),
        yaxis=dict(showgrid=True, gridcolor='rgba(255,255,255,0.05)'),
        margin=dict(l=40, r=40, t=50, b=40)
    )
    
    # 5. Export Datasets
    hist_df = pd.DataFrame({"Date": dates, "Actual": values})
    fore_df = pd.DataFrame({"Date": future_dates, "Forecast": forecast_values, "Lower Bound (95%)": lower_bound, "Upper Bound (95%)": upper_bound})
    
    df_combined = pd.concat([hist_df, fore_df], ignore_index=True)
    
    out_csv = tempfile.mktemp(suffix=".csv")
    df_combined.to_csv(out_csv, index=False)
    
    # Build a nice preview table
    preview_df = fore_df.round(3)
    
    # Calculate simple evaluation stats
    mean_val = np.mean(values)
    stats_html = f"""
    <div style='display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1.5rem;'>
        <div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
            <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Historical Average</div>
            <div style='font-size: 1.8rem; font-weight: bold; margin-top: 0.5rem;'>{mean_val:.3f}</div>
        </div>
        <div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
            <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Final Projection</div>
            <div style='font-size: 1.8rem; font-weight: bold; margin-top: 0.5rem;'>{forecast_values[-1]:.3f}</div>
        </div>
        <div style='background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 8px; padding: 1rem; text-align: center;'>
            <div style='font-size: 0.75rem; text-transform: uppercase; color: #ff7043; letter-spacing: 0.1em;'>Standard Error (Baseline)</div>
            <div style='font-size: 1.8rem; font-weight: bold; margin-top: 0.5rem;'>{std_err:.3f}</div>
        </div>
    </div>
    """
    
    return "", stats_html, fig, preview_df, gr.update(value=out_csv, visible=True)

theme = gr.themes.Default(
    primary_hue="orange",
    neutral_hue="stone"
).set(
    body_background_fill="#0d0907",
    body_text_color="#c4bbae",
    block_background_fill="#16100c",
    block_border_width="1px",
    block_label_text_color="#f4eee6"
)

with gr.Blocks(theme=theme, title="Predictive Modeler Studio") as demo:
    gr.Markdown(
        """
        # 📈 Chronological Predictive Modeler
        ### Upload chronological time-series data to forecast trends and model future values. Perfect for analyzing economic shifts, population growth, or cultural metrics over time.
        """
    )
    
    error_msg = gr.Markdown("", visible=False)
    
    with gr.Row():
        with gr.Column(scale=1):
            file_obj = gr.File(label="Upload Time-Series Sheet", file_types=[".csv", ".xlsx"])
            gr.Markdown("💡 **Tip**: Make sure your file has a **Date/Time** column (first) and a **Numerical value** column (second).")
            
            forecast_steps = gr.Slider(
                minimum=3, 
                maximum=50, 
                value=12, 
                step=1, 
                label="Forecast Steps Ahead",
                info="Number of periods (e.g. months, years) to forecast into the future."
            )
            
            model_type = gr.Radio(
                choices=["Linear Trend", "Polynomial Trend (Quadratic)", "Holt-Winters Exponential Smoothing"],
                value="Holt-Winters Exponential Smoothing",
                label="Forecasting Model"
            )
            
            btn = gr.Button("Calculate Trend & Forecast", variant="primary")
            
        with gr.Column(scale=2):
            stats_box = gr.HTML()
            
            with gr.Tabs():
                with gr.TabItem("Interactive Trend Forecast Chart"):
                    plot_box = gr.Plot()
                with gr.TabItem("Forecast Predictions Table"):
                    table_box = gr.Dataframe(headers=["Date", "Forecast", "Lower Bound (95%)", "Upper Bound (95%)"])
                    download_btn = gr.File(label="Download Combined Historical + Forecast CSV", visible=False)

    def process(file_obj, steps, model):
        err, stats, plot, table, csv_path = run_forecast(file_obj, steps, model)
        if err:
            return gr.update(value=err, visible=True), "", None, None, gr.update(visible=False)
        return gr.update(visible=False), stats, plot, table, csv_path

    btn.click(
        process,
        inputs=[file_obj, forecast_steps, model_type],
        outputs=[error_msg, stats_box, plot_box, table_box, download_btn]
    )

if __name__ == "__main__":
    demo.launch()