mmmapms commited on
Commit
d709518
1 Parent(s): 993ece7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +56 -272
app.py CHANGED
@@ -84,19 +84,17 @@ def simplify_model_names_in_index(df):
84
 
85
  return df
86
 
87
- current_hour, after_10_min = get_current_time()
88
-
89
- github_token = st.secrets["GitHub_Token_KUL_Margarida"]
90
 
91
  if github_token:
92
- forecast_dict = load_forecast(github_token, current_hour, after_10_min)
93
 
94
- historical_forecast = load_GitHub(github_token, 'Historical_forecast.csv', current_hour, after_10_min)
95
 
96
- Data_BE = load_GitHub(github_token, 'BE_Elia_Entsoe_UTC.csv', current_hour, after_10_min)
97
- Data_FR = load_GitHub(github_token, 'FR_Entsoe_UTC.csv', current_hour, after_10_min)
98
- Data_NL = load_GitHub(github_token, 'NL_Entsoe_UTC.csv', current_hour, after_10_min)
99
- Data_DE = load_GitHub(github_token, 'DE_Entsoe_UTC.csv', current_hour, after_10_min)
100
 
101
  Data_BE=convert_European_time(Data_BE, 'Europe/Brussels')
102
  Data_FR=convert_European_time(Data_FR, 'Europe/Paris')
@@ -107,29 +105,6 @@ if github_token:
107
  else:
108
  print("Please enter your GitHub Personal Access Token to proceed.")
109
 
110
- def conformal_predictions(data, target, my_forecast):
111
- data['Residuals'] = data[my_forecast] - data[actual_col]
112
- data['Hour'] = data.index.hour
113
-
114
- min_date = data.index.min()
115
- for date in data.index.normalize().unique():
116
- if date >= min_date + pd.DateOffset(days=30):
117
- start_date = date - pd.DateOffset(days=30)
118
- end_date = date
119
- calculation_window = data[start_date:end_date-pd.DateOffset(hours=1)]
120
- quantiles = calculation_window.groupby('Hour')['Residuals'].quantile(0.8)
121
- # Use .loc to safely access and modify data
122
- if date in data.index:
123
- current_day_data = data.loc[date.strftime('%Y-%m-%d')]
124
- for hour in current_day_data['Hour'].unique():
125
- if hour in quantiles.index:
126
- hour_quantile = quantiles[hour]
127
- idx = (data.index.normalize() == date) & (data.Hour == hour)
128
- data.loc[idx, 'Quantile_80'] = hour_quantile
129
- data.loc[idx, 'Lower_Interval'] = data.loc[idx, my_forecast] - hour_quantile
130
- data.loc[idx, 'Upper_Interval'] = data.loc[idx, my_forecast] + hour_quantile
131
- #data.reset_index(inplace=True)
132
- return data
133
 
134
  # Main layout of the app
135
  col1, col2 = st.columns([5, 2]) # Adjust the ratio to better fit your layout needs
@@ -151,6 +126,7 @@ upper_space.markdown("""
151
  """, unsafe_allow_html=True)
152
 
153
 
 
154
  countries = {
155
  'Netherlands': 'NL',
156
  'Germany': 'DE',
@@ -242,9 +218,7 @@ if section == 'Data':
242
 
243
  st.write('The table below presents the data quality metrics for various energy-related datasets, focusing on the percentage of missing values and the occurrence of extreme or nonsensical values for the selected country.')
244
  data_quality=data.iloc[:-28]
245
- if country_code=='BE':
246
- data_quality=data.iloc[:-5*24]
247
- print(data_quality.tail(48))
248
  # Report % of missing values
249
  missing_values = data_quality[forecast_columns].isna().mean() * 100
250
  missing_values = missing_values.round(2)
@@ -320,61 +294,16 @@ elif section == 'Forecasts':
320
  'Load_entsoe','Load_forecast_entsoe','Wind_onshore_entsoe','Wind_onshore_forecast_entsoe','Wind_offshore_entsoe','Wind_offshore_forecast_entsoe','Solar_entsoe','Solar_forecast_entsoe']
321
  num_per_var=2
322
 
323
- if country_code=='BE':
324
- operation_forecast_load=forecast_dict['Predictions_10h.csv'].filter(like='Load_', axis=1)
325
- operation_forecast_res=forecast_dict['Predictions_17h.csv'].filter(regex='^(?!Load_)')
326
- operation_forecast_load.columns = [col.replace('_entsoe.', '_').replace('Naive.7D', 'WeeklyNaiveSeasonal') for col in operation_forecast_load.columns]
327
- operation_forecast_res.columns = [col.replace('_entsoe.', '_').replace('Naive.1D', 'DailyNaiveSeasonal') for col in operation_forecast_res.columns]
328
- Historical_and_Load=add_feature(operation_forecast_load, historical_forecast)
329
- Historical_and_operational=add_feature(operation_forecast_res, Historical_and_Load)
330
-
331
- best_forecast = Historical_and_operational.filter(like='Forecast_elia', axis=1)
332
- df_combined = Historical_and_operational.join(Data_BE, how='inner')
333
- last_week_best_forecast = best_forecast.loc[best_forecast.index >= (best_forecast.index[-24] - pd.Timedelta(days=7))]
334
- num_per_var=3
335
- forecast_columns_line=['Load_entsoe','Load_forecast_entsoe', 'Load_LightGBMModel.7D.TimeCov.Temp.Forecast_elia', 'Wind_onshore_entsoe','Wind_onshore_forecast_entsoe','Wind_onshore_LightGBMModel.1D.TimeCov.Temp.Forecast_elia','Wind_offshore_entsoe','Wind_offshore_forecast_entsoe','Wind_offshore_LightGBMModel.1D.TimeCov.Temp.Forecast_elia','Solar_entsoe','Solar_forecast_entsoe', 'Solar_LightGBMModel.1D.TimeCov.Temp.Forecast_elia']
336
- else:
337
- forecast_columns_line=forecast_columns
338
 
339
  for i in range(0, len(forecast_columns_line), num_per_var):
340
  actual_col = forecast_columns_line[i]
341
  forecast_col = forecast_columns_line[i + 1]
342
- if country_code=='BE':
343
- my_forecast = forecast_columns_line[i + 2]
344
-
345
 
346
  if forecast_col in data.columns:
347
  fig = go.Figure()
348
  fig.add_trace(go.Scatter(x=last_week.index, y=last_week[actual_col], mode='lines', name='Actual'))
349
  fig.add_trace(go.Scatter(x=last_week.index, y=last_week[forecast_col], mode='lines', name='Forecast ENTSO-E'))
350
-
351
- if country_code=='BE':
352
- conformal=conformal_predictions(df_combined, actual_col, my_forecast)
353
- last_week_conformal = conformal.loc[conformal.index >= (conformal.index[-24] - pd.Timedelta(days=7))]
354
- if actual_col =='Load_entsoe':
355
- last_week_conformal = conformal.loc[conformal.index >= (conformal.index[-24] - pd.Timedelta(days=5))]
356
- fig.add_trace(go.Scatter(x=last_week_best_forecast.index, y=last_week_best_forecast[my_forecast], mode='lines', name='Forecast EDS'))
357
-
358
- fig.add_trace(go.Scatter(
359
- x=last_week_conformal.index,
360
- y=last_week_conformal['Lower_Interval'],
361
- mode='lines',
362
- line=dict(width=0),
363
- showlegend=False
364
- ))
365
-
366
- # Add the upper interval trace and fill to the lower interval
367
- fig.add_trace(go.Scatter(
368
- x=last_week_conformal.index,
369
- y=last_week_conformal['Upper_Interval'],
370
- mode='lines',
371
- line=dict(width=0),
372
- fill='tonexty', # Fill between this trace and the previous one
373
- fillcolor='rgba(68, 68, 68, 0.3)',
374
- name='P10/P90 prediction intervals'
375
- ))
376
-
377
-
378
  fig.update_layout(title=f'Forecasts vs Actual for {actual_col}', xaxis_title='Date', yaxis_title='Value [MW]')
379
 
380
  st.plotly_chart(fig)
@@ -631,33 +560,6 @@ elif section == 'Forecasts':
631
  )
632
 
633
  return fig
634
-
635
-
636
-
637
-
638
- if country_code == "BE":
639
-
640
- st.header('MAE Ratio Comparison by Forecast Hour')
641
- st.write("These clock-plots shows the relative Mean Absolute Error (rMAE) of different forecasting models compared to the ENTSO-E forecast, by the hour at which the forecast was made. "
642
- "The rMAE is calculated as the ratio of the model's MAE to the ENTSO-E forecast's MAE.")
643
-
644
- forecast_dict2 = forecast_dict.copy()
645
- forecast_dict2 = {k: simplify_model_names(v) for k, v in forecast_dict.items()}
646
-
647
-
648
- mae_comparison_fig = plot_mae_comparison_clock(forecast_dict2, 'Solar', 'rMAE Ratio Comparison for Solar', real_values_df=Data_BE)
649
- st.plotly_chart(mae_comparison_fig)
650
-
651
- mae_comparison_fig_wind_onshore = plot_mae_comparison_clock(forecast_dict2, 'Wind_onshore', 'MAE Ratio Comparison for Wind Onshore', real_values_df=Data_BE)
652
- st.plotly_chart(mae_comparison_fig_wind_onshore)
653
-
654
- mae_comparison_fig_wind_offshore = plot_mae_comparison_clock(forecast_dict2, 'Wind_offshore', 'MAE Ratio Comparison for Wind Offshore', real_values_df=Data_BE)
655
- st.plotly_chart(mae_comparison_fig_wind_offshore)
656
-
657
- mae_comparison_fig_load = plot_mae_comparison_clock(forecast_dict2, 'Load', 'MAE Ratio Comparison for Load', real_values_df=Data_BE)
658
- st.plotly_chart(mae_comparison_fig_load)
659
-
660
-
661
 
662
 
663
  # Scatter plots for error distribution
@@ -683,177 +585,59 @@ elif section == 'Forecasts':
683
  output_text = f"The below metrics are calculated from the selected date range from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}. This interval can be adjusted from the sidebar."
684
  st.write(output_text)
685
 
 
 
686
 
687
- if country_code == "BE":
688
-
689
- # Combine the two DataFrames on their index
690
- df_combined = Historical_and_operational.join(Data_BE, how='inner')
691
- # List of model columns from historical_forecast
692
- model_columns = historical_forecast.columns
693
-
694
- # Initialize dictionaries to store MAE and RMSE results for each variable
695
- results_wind_onshore = {}
696
- results_wind_offshore = {}
697
- results_load = {}
698
- results_solar = {}
699
-
700
- # Mapping of variables to their corresponding naive models
701
- naive_models = {
702
- 'Wind_onshore': 'Wind_onshore_DailyNaiveSeasonal',
703
- 'Wind_offshore': 'Wind_offshore_DailyNaiveSeasonal',
704
- 'Load': 'Load_WeeklyNaiveSeasonal',
705
- 'Solar': 'Solar_DailyNaiveSeasonal'
706
- }
707
-
708
- # Step 1: Calculate MAE, RMSE, and rMAE for each model
709
- for col in model_columns:
710
- # Extract the variable name by taking everything before the first underscore
711
- base_variable = col.split('_')[0]
712
-
713
- # Handle cases where variable names might be combined with multiple parts (e.g., "Load_LightGBMModel...")
714
- if base_variable in ['Wind', 'Load', 'Solar']:
715
- if 'onshore' in col:
716
- variable_name = 'Wind_onshore'
717
- results_dict = results_wind_onshore
718
- elif 'offshore' in col:
719
- variable_name = 'Wind_offshore'
720
- results_dict = results_wind_offshore
721
- else:
722
- variable_name = base_variable
723
- results_dict = results_load if base_variable == 'Load' else results_solar
724
  else:
725
- variable_name = base_variable
726
-
727
- # Construct the corresponding `variable_entsoe` column name
728
- entsoe_column = f'{variable_name}_entsoe'
729
- naive_model_col = naive_models.get(variable_name, None)
730
-
731
- # Drop NaNs for the specific pair of columns before calculating MAE and RMSE
732
- if entsoe_column in df_combined.columns and naive_model_col in df_combined.columns:
733
- valid_data = df_combined[[col, entsoe_column]].dropna()
734
- valid_naive_data = df_combined[[entsoe_column, naive_model_col]].dropna()
735
-
736
- # Calculate MAE and RMSE for the model against the `variable_entsoe`
737
- mae = np.mean(abs(valid_data[col] - valid_data[entsoe_column]))
738
- rmse = np.sqrt(mean_squared_error(valid_data[col], valid_data[entsoe_column]))
739
-
740
- # Calculate MAE for the Naive model
741
- mae_naive = np.mean(abs(valid_naive_data[entsoe_column] - valid_naive_data[naive_model_col]))
742
-
743
- # Calculate rMAE for the model
744
- rMAE = mae / mae_naive if mae_naive != 0 else np.inf
745
-
746
- # Store the results in the corresponding dictionary
747
- results_dict[f'{col}'] = {'MAE': mae, 'RMSE': rmse, 'rMAE': rMAE}
748
-
749
- # Step 2: Calculate MAE, RMSE, and rMAE for ENTSO-E forecasts specifically
750
- for variable_name in naive_models.keys():
751
- entsoe_column = f'{variable_name}_entsoe'
752
- forecast_entsoe_column = f'{variable_name}_forecast_entsoe'
753
- naive_model_col = naive_models[variable_name]
754
-
755
- # Ensure that the ENTSO-E forecast is included in the results
756
- if forecast_entsoe_column in df_combined.columns:
757
- valid_data = df_combined[[forecast_entsoe_column, entsoe_column]].dropna()
758
- valid_naive_data = df_combined[[entsoe_column, naive_model_col]].dropna()
759
-
760
- # Calculate MAE and RMSE for the ENTSO-E forecast against the actuals
761
- mae_entsoe = np.mean(abs(valid_data[forecast_entsoe_column] - valid_data[entsoe_column]))
762
- rmse_entsoe = np.sqrt(mean_squared_error(valid_data[forecast_entsoe_column], valid_data[entsoe_column]))
763
-
764
- # Calculate rMAE for the ENTSO-E forecast
765
- mae_naive = np.mean(abs(valid_naive_data[entsoe_column] - valid_naive_data[naive_model_col]))
766
- rMAE_entsoe = mae_entsoe / mae_naive if mae_naive != 0 else np.inf
767
-
768
- # Add the ENTSO-E results to the corresponding dictionary
769
- if variable_name == 'Wind_onshore':
770
- results_wind_onshore[forecast_entsoe_column] = {'MAE': mae_entsoe, 'RMSE': rmse_entsoe, 'rMAE': rMAE_entsoe}
771
- elif variable_name == 'Wind_offshore':
772
- results_wind_offshore[forecast_entsoe_column] = {'MAE': mae_entsoe, 'RMSE': rmse_entsoe, 'rMAE': rMAE_entsoe}
773
- elif variable_name == 'Load':
774
- results_load[forecast_entsoe_column] = {'MAE': mae_entsoe, 'RMSE': rmse_entsoe, 'rMAE': rMAE_entsoe}
775
- elif variable_name == 'Solar':
776
- results_solar[forecast_entsoe_column] = {'MAE': mae_entsoe, 'RMSE': rmse_entsoe, 'rMAE': rMAE_entsoe}
777
-
778
- # Convert the dictionaries to DataFrames and sort by rMAE
779
- df_wind_onshore = pd.DataFrame.from_dict(results_wind_onshore, orient='index').sort_values(by='rMAE')
780
- df_wind_offshore = pd.DataFrame.from_dict(results_wind_offshore, orient='index').sort_values(by='rMAE')
781
- df_load = pd.DataFrame.from_dict(results_load, orient='index').sort_values(by='rMAE')
782
- df_solar = pd.DataFrame.from_dict(results_solar, orient='index').sort_values(by='rMAE')
783
-
784
-
785
- st.write("##### Wind Onshore:")
786
- df_wind_onshore = simplify_model_names_in_index(df_wind_onshore)
787
- st.dataframe(df_wind_onshore)
788
-
789
- st.write("##### Wind Offshore:")
790
- df_wind_offshore2 = simplify_model_names_in_index(df_wind_offshore)
791
- st.dataframe(df_wind_offshore)
792
-
793
- st.write("##### Load:")
794
- df_load = simplify_model_names_in_index(df_load)
795
- st.dataframe(df_load)
796
 
797
- st.write("##### Solar:")
798
- df_solar = simplify_model_names_in_index(df_solar)
799
- st.dataframe(df_solar)
800
 
 
801
 
 
 
802
 
803
- else:
804
- data = data.loc[start_date:end_date]
805
- accuracy_metrics = pd.DataFrame(columns=['MAE', 'rMAE'], index=['Load', 'Solar', 'Wind Onshore', 'Wind Offshore'])
806
-
807
- for i in range(0, len(forecast_columns), 2):
808
- actual_col = forecast_columns[i]
809
- forecast_col = forecast_columns[i + 1]
810
- if forecast_col in data.columns:
811
- obs = data[actual_col]
812
- pred = data[forecast_col]
813
- error = pred - obs
814
-
815
- mae = round(np.mean(np.abs(error)),2)
816
- if 'Load' in actual_col:
817
- persistence = obs.shift(168) # Weekly persistence
818
- else:
819
- persistence = obs.shift(24) # Daily persistence
820
-
821
- # Using the whole year's data for rMAE calculations
822
- rmae = round(mae / np.mean(np.abs(obs - persistence)),2)
823
-
824
- row_label = 'Load' if 'Load' in actual_col else 'Solar' if 'Solar' in actual_col else 'Wind Offshore' if 'Wind_offshore' in actual_col else 'Wind Onshore'
825
- accuracy_metrics.loc[row_label] = [mae, rmae]
826
-
827
- accuracy_metrics.dropna(how='all', inplace=True)# Sort by rMAE (second column)
828
- accuracy_metrics.sort_values(by=accuracy_metrics.columns[1], ascending=True, inplace=True)
829
- accuracy_metrics = accuracy_metrics.round(4)
830
-
831
- col1, col2 = st.columns([3, 2])
832
-
833
- with col1:
834
- st.dataframe(accuracy_metrics)
835
-
836
- with col2:
837
- st.markdown("""
838
- <style>
839
- .big-font {
840
- font-size: 20px;
841
- font-weight: 500;
842
- }
843
- </style>
844
- <div class="big-font">
845
- Equations
846
- </div>
847
- """, unsafe_allow_html=True)
848
-
849
- st.markdown(r"""
850
- $\text{MAE} = \frac{1}{n}\sum_{i=1}^{n}|y_i - \hat{y}_i|$
851
-
852
-
853
- $\text{rMAE} = \frac{\text{MAE}}{MAE_{\text{Persistence Model}}}$
854
-
855
 
856
- """)
857
 
858
 
859
 
 
84
 
85
  return df
86
 
87
+ github_token = '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'
 
 
88
 
89
  if github_token:
90
+ forecast_dict = load_forecast(github_token)
91
 
92
+ historical_forecast=load_GitHub(github_token, 'Historical_forecast.csv')
93
 
94
+ Data_BE=load_GitHub(github_token, 'BE_Elia_Entsoe_UTC.csv')
95
+ Data_FR=load_GitHub(github_token, 'FR_Entsoe_UTC.csv')
96
+ Data_NL=load_GitHub(github_token, 'NL_Entsoe_UTC.csv')
97
+ Data_DE=load_GitHub(github_token, 'DE_Entsoe_UTC.csv')
98
 
99
  Data_BE=convert_European_time(Data_BE, 'Europe/Brussels')
100
  Data_FR=convert_European_time(Data_FR, 'Europe/Paris')
 
105
  else:
106
  print("Please enter your GitHub Personal Access Token to proceed.")
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
 
109
  # Main layout of the app
110
  col1, col2 = st.columns([5, 2]) # Adjust the ratio to better fit your layout needs
 
126
  """, unsafe_allow_html=True)
127
 
128
 
129
+
130
  countries = {
131
  'Netherlands': 'NL',
132
  'Germany': 'DE',
 
218
 
219
  st.write('The table below presents the data quality metrics for various energy-related datasets, focusing on the percentage of missing values and the occurrence of extreme or nonsensical values for the selected country.')
220
  data_quality=data.iloc[:-28]
221
+
 
 
222
  # Report % of missing values
223
  missing_values = data_quality[forecast_columns].isna().mean() * 100
224
  missing_values = missing_values.round(2)
 
294
  'Load_entsoe','Load_forecast_entsoe','Wind_onshore_entsoe','Wind_onshore_forecast_entsoe','Wind_offshore_entsoe','Wind_offshore_forecast_entsoe','Solar_entsoe','Solar_forecast_entsoe']
295
  num_per_var=2
296
 
297
+ forecast_columns_line=forecast_columns
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
  for i in range(0, len(forecast_columns_line), num_per_var):
300
  actual_col = forecast_columns_line[i]
301
  forecast_col = forecast_columns_line[i + 1]
 
 
 
302
 
303
  if forecast_col in data.columns:
304
  fig = go.Figure()
305
  fig.add_trace(go.Scatter(x=last_week.index, y=last_week[actual_col], mode='lines', name='Actual'))
306
  fig.add_trace(go.Scatter(x=last_week.index, y=last_week[forecast_col], mode='lines', name='Forecast ENTSO-E'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  fig.update_layout(title=f'Forecasts vs Actual for {actual_col}', xaxis_title='Date', yaxis_title='Value [MW]')
308
 
309
  st.plotly_chart(fig)
 
560
  )
561
 
562
  return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
 
564
 
565
  # Scatter plots for error distribution
 
585
  output_text = f"The below metrics are calculated from the selected date range from {start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}. This interval can be adjusted from the sidebar."
586
  st.write(output_text)
587
 
588
+ data = data.loc[start_date:end_date]
589
+ accuracy_metrics = pd.DataFrame(columns=['MAE', 'rMAE'], index=['Load', 'Solar', 'Wind Onshore', 'Wind Offshore'])
590
 
591
+ for i in range(0, len(forecast_columns), 2):
592
+ actual_col = forecast_columns[i]
593
+ forecast_col = forecast_columns[i + 1]
594
+ if forecast_col in data.columns:
595
+ obs = data[actual_col]
596
+ pred = data[forecast_col]
597
+ error = pred - obs
598
+
599
+ mae = round(np.mean(np.abs(error)),2)
600
+ if 'Load' in actual_col:
601
+ persistence = obs.shift(168) # Weekly persistence
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  else:
603
+ persistence = obs.shift(24) # Daily persistence
604
+
605
+ # Using the whole year's data for rMAE calculations
606
+ rmae = round(mae / np.mean(np.abs(obs - persistence)),2)
607
+
608
+ row_label = 'Load' if 'Load' in actual_col else 'Solar' if 'Solar' in actual_col else 'Wind Offshore' if 'Wind_offshore' in actual_col else 'Wind Onshore'
609
+ accuracy_metrics.loc[row_label] = [mae, rmae]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
+ accuracy_metrics.dropna(how='all', inplace=True)# Sort by rMAE (second column)
612
+ accuracy_metrics.sort_values(by=accuracy_metrics.columns[1], ascending=True, inplace=True)
613
+ accuracy_metrics = accuracy_metrics.round(4)
614
 
615
+ col1, col2 = st.columns([3, 2])
616
 
617
+ with col1:
618
+ st.dataframe(accuracy_metrics)
619
 
620
+ with col2:
621
+ st.markdown("""
622
+ <style>
623
+ .big-font {
624
+ font-size: 20px;
625
+ font-weight: 500;
626
+ }
627
+ </style>
628
+ <div class="big-font">
629
+ Equations
630
+ </div>
631
+ """, unsafe_allow_html=True)
632
+
633
+ st.markdown(r"""
634
+ $\text{MAE} = \frac{1}{n}\sum_{i=1}^{n}|y_i - \hat{y}_i|$
635
+
636
+
637
+ $\text{rMAE} = \frac{\text{MAE}}{MAE_{\text{Persistence Model}}}$
638
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
 
640
+ """)
641
 
642
 
643