ASHRAE GREAT ENERGY PREDICTION(III)

TABLE OF CONTENTS

  1. Introduction

1) Introduction

This is the third energy competition which is held by ASHRAE in the year 2019.

This competition is different from the other two competitions held in the past cause the dataset provided in this spans over a 3 year timeframe(Jan 2016-Dec 2018) whereas for the other two only few months of data was available.Larger dataset resulted in more generalization of the ML Models.Here we have to predict the energy consumption for four different types of meters(Steam,Electricity,ChilledWater,Hotwater).

The training dataset contains reading from 1448 buildings taken from 16 different sites of North America and Europe.The weather conditions and information about the buildings were also provided.

2) Business Problem

In todays world in the energy sector we always see that the industries are coming up with new ways to reduce the emission of the greenhouse gases and also deploying new ways to cut the cost of energy consumption.Due to this the renewable and smart grid sectors are expanding but not at that much rate as it should be if we consider the present environmental conditions.

So the question is why they are not able to expand at a faster rate although they have come up with efficient methods of lowering down the energy consumption.

This is due to the fact that the power sector industries are not having the exact methodology of calculating the savings in the post retrofit period.

Now if the power sector industries can get a model which can predict the energy consumption of the buildings accurately based on the historical energy usage then they can compare it with the energy consumption in the post retrofit period which can help them to calculate the energy savings effectively.This will also lead to better cost optimization and incentives.

Now who is going to build these models.This is where data scientists and machine learning practitioners come in.They are going to build these counterfactual models which will predict the meter readings based on the historical energy usage.

3) ML Formulation

As we have to predict the meter readings which are real valued numbers so the given problem can be posed as regression based ml problem.

The first year of hourly meter data was provided as the training set along with the weather conditions and the building metadata.

Next two years of data was provided as the test set on which we have to make our predictions.

4) Performance-Metric

The performance metric used here is the Root Mean Squared Logarithmic Error(RMSLE).

Now I will explain why we are using this metric for our regression problem why not use RMSE.

RMSLE incurs large penalty for the underestimation of the actual variable as compared to RMSE.This is very useful for our business case as underestimation of our target variable is not accepted.For Ex- If we are underestimating our electricity consumption then this can lead to shutdown of buildings during business hours which can lead to monetary losses.This point indeed is the most important point for our use case.

In regression based models RMSE is very much affected by outliers.Now if we take the case of RMSLE it is much more robust to outliers as it takes the log1p of the values which kind of compresses the value.

N is the total number of observations the public/private dataset

yi is the actual target variable

yi^ is the predicted target variable

log(x) is the natural logarithm of x

Source →https://www.google.com/url?q=https://www.kaggle.com/c/ashrae-energy-prediction/overview/evaluation&sa=D&ust=1602591028562000&usg=AOvVaw2Pp9JH8h-5U0AgWobeEeml

5) Dataset Analysis

The training dataset consists of 20 million data points with the hourly meter readings for each meter.The weather conditions and building metadata was also provided.This dataset spans over one year timeframe(Jan 2016-Dec2016).

The test dataset consists about 41 million data points which spans over 2 year timeperiod from Jan 2017 to Dec 2018.

Train.csv →Building_id,Timestamp,Meter(0-Electricity,1-Chilledwater,2-Steam,3-Hotwater)

Weather_train.csv→Site_id,Timestamp,wind_speed,wind_direction,cloud_coverage,air_temperature,precipitation_depth_1_hr,dew_temperature,sea_level_pressure

Building_metadata.csv→Building_id,floor_count,square_feet,year_built,primary_usage

Site_id →Unique id of the site.It ranges from 0–15

Building_id →unique id of the building.It ranges from 0–1448

Sources →https://www.kaggle.com/c/ashrae-energy-prediction/data

6) Exploratory Data Analysis

Here first what i did is merged the train data with building metadata and weather data.Now after getting my final train dataset I started analyzing each building site by site.

  1. Interesting Findings

As I started analyzing meter readings for each building for every site what i found is that the meter readings for site 0 and meter 0 were mostly zero till May 20 2016.Below is the image shown along with the code snippet.

df.drop(index=df[(df['building_id']<=104) & (df['meter']==0) & (df['timestamp']<'2016-05-21')].index,inplace=True)

There were many buildings with irregular or very high meter readings which also needs to be removed.These buildings are distributed across many sites with different meters.below is the code for it

df.drop(index=df[(df['building_id']==53) & (df['meter']==0)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==1099) & (df['meter']==2)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==1250) & (df['meter']==2)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==1227) & (df['meter']==0)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==1314) & (df['meter']==0)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==1281) & (df['meter']==0)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==279) & (df['meter']==3)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==263) & (df['meter']==3)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==287) & (df['meter']==3)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==1018) & (df['meter']==1)].index,inplace=True)#Removing Anamolous Building
df.drop(index=df[(df['building_id']==1022) & (df['meter']==1)].index,inplace=True)#Removing Anamolous Building

The air timestamp were not aligned with the local timestamp of the hourly meter readings.Out of 15 different sites this problem was observed for 13 different sites.below is the image for a single site but the same was observed for 12 more sites where the trmperature was maxing around 19:00 pm to 23:00 pm.

I have also checked the energy consumption of different meters across the hour of the day to see for the energy usage patterns.Here we can see that the electricity meter reading starts increasing from 6:00 am in the morning and starts decreasing after 18:00 pm.

If we want to check for chilled water reading it generally peaks around during the afternoon hours and starts decreasing from evening hours.

Hotwater and steam are not showing any considerable patterns during the hour of the day.

Lets further investigate the energy pattern over the day of the week.

The first one which I have shown here is the electricity consumption over the day of the week.Here we can observe that the consumption is less for the weekend as compares to the weekdays.This pattern is consistent for almost all the sites when checking for meter 0.Now for the remaining three meters this pattern can be observed more or less the same for most of the sites.

Now coming to the next point here I will show you the energy consumption of meters according to the seasons.The most dominant effect of this is present on the Chilledwater and Hotwater Consumption.

From the above two plots we can see that Chilledwater and Hotwater Consumption shows strong variations according to the seasons.

7) Imputation Techniques

Firstly i will check the precentage of the missing values for each feature present.Below is the code along with the precentage of null values.

(df_train_merge_cleaned.isnull().sum()/df_train_merge_cleaned.shape[0])*100

building_id-> 0.000000 meter-> 0.000000 timestamp-> 0.000000 meter_reading ->0.000000 site_id-> 0.000000 primary_use-> 0.000000 square_feet ->0.000000 year_built-> 61.010820 floor_count-> 82.361341 air_temperature-> 0.486579 cloud_coverage ->43.647889 dew_temperature-> 0.504150 precip_depth_1_hr ->18.948532 sea_level_pressure-> 6.191485 wind_direction-> 7.264534 wind_speed-> 0.724510

Now coming to the imputration part as magnitude of energy consumption depends on individual site.It also depends on the day of the week along with the month.Therefore we need to group it by taking all the three mentioned features.

First i have dropped the floor count as it was missing for more than 80%.

df_train_merge_cleaned.drop('floor_count',axis=1,inplace=True)
df_train_merge_cleaned.reset_index(inplace=True)

Now coming to the other features.This is the code which i used to fill all, the other features except the air and dew temperature.

df_train_merge_cleaned['day']=df_train_merge_cleaned['timestamp'].dt.day
df_train_merge_cleaned['month']=df_train_merge_cleaned['timestamp'].dt.month
cc_fill=df_train_merge_cleaned.groupby(['site_id','day','month'])['cloud_coverage'].median().reset_index()
cc_fill.rename(columns={'cloud_coverage':'cc_filler'},inplace=True)
cc_fill['cc_filler'].fillna(method='ffill',inplace=True)
df_train_merge_cleaned=df_train_merge_cleaned.merge(cc_fill,how='left',on=['site_id','day','month'])
df_train_merge_cleaned['cloud_coverage'].fillna(df_train_merge_cleaned['cc_filler'],inplace=True)
df_train_merge_cleaned.drop(labels=['cc_filler'],axis=1,inplace=True)

For air and dew temperature as it was showing strong correlation with the previous hour data.Therefore I used linear interpolation to fill up the missing values.

z=df_train.set_index(‘timestamp’).copy()

plot_pacf(z[‘air_temperature’].resample(‘1h’).mean(),lags=24)

df_train_merge_cleaned[‘air_temperature’]=df_train_merge_cleaned[‘air_temperature’].interpolate(method=’linear’) df_train_merge_cleaned[‘dew_temperature’]=df_train_merge_cleaned[‘dew_temperature’].interpolate(method=’linear’)

8) Feature Engineering

Firstly what i did is aligned all the air temperature with the local timestamp of the meter readings.

Now for site 0 the air temperature was shifted by 5 hours.This was different for different sites but the code is exactly the same.Below is the code for the timestamp alignment

df_train_site_0=df_train_merge_cleaned_imputed[df_train_merge_cleaned_imputed['site_id']==0]
df_train_site_0.reset_index(inplace=True)
df_train_site_0['timestamp_aligned']=df_train_site_0['timestamp']-timedelta(hours=5,minutes=0)
df_air_temp_timestamp=df_train_site_0[['timestamp_aligned','building_id','meter','air_temperature']].copy()
df_air_temp_timestamp.rename(columns={'timestamp_aligned':'timestamp'},inplace=True)
df_train_site_0.drop(['air_temperature','timestamp_aligned'],axis=1,inplace=True)
df_train_site_0['air_temperature_aligned']=df_air_temp_timestamp[df_air_temp_timestamp['timestamp'].isin(df_train_site_0['timestamp'])].reset_index(drop=True)['air_temperature']
df_train_site_0['air_temperature_aligned']=df_train_site_0['air_temperature_aligned'].interpolate()
df_train_site_0.rename(columns={'air_temperature_aligned':'air_temperature'},inplace=True)
df_train_site_0.drop(['level_0','index'],axis=1,inplace=True)

1)RELATIVE HUMIDITY→HUMIDITY AFFECTS THE ENERGY REQUIREMENTS OF THE BUILDINGS.AS HUMIDITY INCREASES THE LOAD ON THE HVAC SYSTEM INCREASES DUE TO WHICH THE METER READINGS ARE AFFECTED.THIS FEATURE WILL HELP THE MODEL TO LEARN THE ENRGY REQUIREMENTS ACCORDING TO THE HUMIDITY FACTOR.BELOW IS THE CODE FOR IT

saturated_vapor_pressure = 6.11 * (10**(7.5*df_train_merged_final['air_temperature']/(237.3+df_train_merged_final['air_temperature'])))
actual_vapor_pressure = 6.11 * (10**(7.5*df_train_merged_final['dew_temperature']/(237.3+df_train_merged_final['dew_temperature'])))
df_train_merged_final['relative_humidity']=(actual_vapor_pressure/saturated_vapor_pressure)*100

2)IS SUMMER MONTH & IS WINTER MONTH → CHILLED WATER HOT WATER AND STEAM READINGS SHOW CONSIDERABLE PATTERN OVER THE SEASONS.THIS FEATURE WILL HELP THE MODEL TO LEARN THAT.BELOW IS THE CODE FOR IT

df_train_merged_final[‘is_winter_month’]=(df_train_merged_final[‘month’].isin([12,1,2])).astype(int) df_train_merged_final[‘is_summer_month’]=(df_train_merged_final[‘month’].isin([6,7,8])).astype(int)

3) HOLIDAYS →AS THE ENERGY REQUIREMENTS MIGHT BE DIFFERENT DURING THE HOLIDAY AS COMPARED TO THE OTHER DAYS INCLUDING THIS FEATURE WILL HELP THE MODEL TO LEARN THAT.BELOW IS THE CODE FOR IT

holiday_datetime=pd.to_datetime(holidays,yearfirst=True) df_train_merged_final[‘is_pub_holiday’]=(df_train_merged_final[‘timestamp’].dt.date.isin(holiday_datetime.date)).astype(int)

4)IS WEEKDAY →THIS FEATURE IS ADDED TO CHECK THE ENRGY REQUIREMENTS OF THE WEEKDAY AS COMPARED TO THE WEEKEND.AS OBSERVED IN THE EDA IT SHOWS A SIGNIFICANT TREND OVER THE WEEK AS COMPARED TO THE WEEKEND FOR ALL THE ENERGY METER.BELOW IS THE CODE FOR IT

df_train_merged_final['is_weekday']=((~df_train_merged_final['timestamp'].dt.date.isin(holiday_datetime.date))&(df_train_merged_final['weekday'].isin([0,1,2,3,4]))).astype(int)

5) BUSY-HOURS →AS THE ENERGY REQUIREMENTS ARE DIFFERENT OVER THE DAY THAN THE NIGHT.SO TO HELP THE MODEL LEARN THAT WE CAN USE BUSY HOURS(6:00 AM TO 18:00 PM).BELOW IS THE CODE FOR IT

z_busy_hours=df_train_merged_final.set_index(['timestamp']).between_time('06:00:00','18:00:00').reset_index()
z_busy_hours_timestamp=[i for i in z_busy_hours['timestamp']]
df_train_merged_final['busy_hours']=((~df_train_merged_final['timestamp'].dt.date.isin(holiday_datetime.date))&(df_train_merged_final['timestamp'].isin(z_busy_hours_timestamp))).astype(int)

6) ADDING BASIC TIMESTAMP FEATURES SUCH AS DAY MONTH AND HOUR.BELOW IS THE CODE FOR IT

df_train_merged_final['hour']=df_train_merged_final['timestamp'].dt.hourdf_train_merged_final['weekday']=df_train_merged_final['timestamp'].dt.weekdaydf_train_merge_cleaned['day']=df_train_merge_cleaned['timestamp'].dt.day
df_train_merge_cleaned['month']=df_train_merge_cleaned['timestamp'].dt.month

7) DOING LABEL ENCODING OF THE CATEGORICAL VARIABLES.BELOW IS THE CODE FOR IT

label_encoder=LabelEncoder()
df_train_merged_final_red['primary_use']=label_encoder.fit_transform(df_train_merged_final_red['primary_use'])

9) Feature Selection

I have done feature selection before fitting my model as the dataset was large so more features would definitely increase the chances of overfitting.I have done it using XGB Regressor as my base model.

As we can see from the above plot there are a total of 17 features which are important and the rest 6 features are discarded.

10) Model and Hyperparameter Tuning

Here I have tried different models such as RF,XGBOOST,LGBM,CATBOOST,CUSTOM-ENSEMBLING and a DEEP LEARNING MODEL.For each model I have performed hyperparameter tuning.Initially only I have applied target transformation(log1p) on my target variable , therefore I am using RMSE as my evaluation metric which by default becomes RMSLE.

  1. XGBOOST MODEL

Hyperparameter Tuning for XGBOOST MODEL with the best params

x_cfl=XGBRegressor(tree_method=’gpu_hist’)

params={‘n_estimators’:[300,500,1000,1500,2000],

‘learning_rate’:[0.01,0.03,0.05,0.1],

‘max_depth’:[3,5,7,9],

‘colsample_bytree’:[0.5,0.8,0.9,1]}

random_clf=RandomizedSearchCV(x_cfl,params,scoring=’neg_root_mean_squared_error’,n_jobs=-1,cv=3,verbose=10,random_state=0,n_iter=10)

random_clf.fit(X_train,y_train)

random_clf.best_params_

{'colsample_bytree': 0.8,
'learning_rate': 0.1,
'max_depth': 9,
'n_estimators': 500}

2) LGBM MODEL

Hyperparameter Tuning for LGBM MODEL with the best params

params={‘max_depth’:[3,5,7,9,11],

‘learning_rate’:[0.1,0.01,0.03,0.05],

‘colsample_bytree’:[0.7,0.8,0.9,1.0],

‘n_estimators’:[300,500,800,1200],

‘min_child_samples’:[50,100,200,300,500]}

lgb_reg=LGBMRegressor()

random_clf=RandomizedSearchCV(lgb_reg,params,n_iter=8,scoring=’neg_root_mean_squared_error’,cv=kf,verbose=24,random_state=1,n_jobs=-1)

random_clf.fit(X_train,y_train)

random_clf.best_params_

{'colsample_bytree': 0.9,
'learning_rate': 0.1,
'max_depth': 7,
'min_child_samples': 100,
'n_estimators': 1200}

3) CATBOOST MODEL

params=[]
err_score=[]
for i in range(10):
max_depth=np.random.randint(3,15)
estimators=np.random.randint(300,1500)




cat_reg=CatBoostRegressor(task_type='GPU',loss_function='RMSE',max_depth=max_depth,n_estimators=estimators,learning_rate=0.1)
cat_reg.fit(X_train,y_train)
test_pred=cat_reg.predict(X_test)
err_test=np.sqrt(mean_squared_error(y_test,test_pred))
err_score.append(err_test)
params.append((max_depth,estimators))

err_score

[0.8014502674080963,
0.969339306037929,
1.1105379937315227,
0.8017482632418587,
1.083261361679219,
0.7968043441249344,
0.88638072298227,
1.2696015010888373,
1.0057081335900901,
1.3935640553815056]

params

[(13, 1183), (10, 467), (5, 1456), (14, 925), (6, 986), (13, 1289), (10, 756), (4, 928), (8, 696), (3, 695)]

From the error score we can see that best params found is max_depth-13 and n_estimators-1289

4) RF REGRESSOR

Hyperparameter tuning with the best params.The best params can be found here

rf_reg=RandomForestRegressor(n_jobs=-1)

params={‘n_estimators’:[20,40,60,80,100],

‘max_depth’:[3,5,7,9]}

random_clf=RandomizedSearchCV(rf_reg,params,scoring=’neg_root_mean_squared_error’,n_jobs=-1,cv=3,verbose=15,n_iter=5,random_state=0)

random_clf.fit(X_train,y_train)

random_clf.best_params_

{'max_depth': 9, 'n_estimators': 100}

5) CUSTOM-ENSEMBLING

Here I divide the train set into 80–20.Now 20% is kept as test set and 80% is further divided into 50–50(df1 and df2).On df1 we take our base models and do hyperparameter tuning and predict on df2.Now we combine the predictions of df2 and that is used as training set for my meta model.As we have the target variable for the rest 20% along with the predictions of df2 is used for hyperparameter tuning.

Further we do the predictions on the final test set and these predictions are again combined and used as an input to meta model.Now the final predictions come from the meta model.

The base models used are CATBOOST,LGBM AND XGBOOST.Meta model used is LGBM.

X_train,X_test,y_train,y_test=train_test_split(df_tr_red_final,y_tr,test_size=0.2,random_state=0)X_train_d1,X_train_d2,y_train_d1,y_train_d2=train_test_split(X_train,y_train,test_size=0.5,random_state=0)s1_d1=X_train_d1.sample(frac=0.8,replace=True,random_state=0)
y1_d1=y_train_d1.sample(frac=0.8,replace=True,random_state=0)
s2_d1=X_train_d1.sample(frac=0.8,replace=True,random_state=1)
y2_d1=y_train_d1.sample(frac=0.8,replace=True,random_state=1)
s3_d1=X_train_d1.sample(frac=0.8,replace=True,random_state=2)
y3_d1=y_train_d1.sample(frac=0.8,replace=True,random_state=2)

Here is the code for hyperparameter tuning of my base models

x_cfl=XGBRegressor(tree_method='gpu_hist')
params={'n_estimators':[300,500,1000,1500,2000],
'learning_rate':[0.01,0.03,0.05,0.1],
'max_depth':[3,5,7,9],
'colsample_bytree':[0.5,0.8,0.9,1]}
random_xgb=RandomizedSearchCV(x_cfl,params,scoring='neg_root_mean_squared_error',n_jobs=-1,cv=3,verbose=10,random_state=1,n_iter=10)
random_xgb.fit(s1_d1,y1_d1)
random_xgb.best_params_{'colsample_bytree': 0.8,
'learning_rate': 0.1,
'max_depth': 7,
'n_estimators': 2000}
params={'max_depth':[3,5,7,9,11,13,15],
'n_estimators':[300,500,800,1000,1200,1500],
'learning_rate':[0.1,0.01,0.03,0.05]}
cat_reg=CatBoostRegressor()
random_cat=RandomizedSearchCV(cat_reg,params,scoring='neg_root_mean_squared_error',n_jobs=-1,cv=3,verbose=1,random_state=1,n_iter=8)
random_cat.fit(s2_d1,y2_d1)
random_cat.best_params_{'learning_rate': 0.1, 'max_depth': 15, 'n_estimators': 1200}params={'max_depth':[3,5,7,9,11],
'learning_rate':[0.1,0.01,0.03,0.05],
'colsample_bytree':[0.7,0.8,0.9,1.0],
'n_estimators':[300,500,800,1200],
'min_child_samples':[50,100,200,300,500]}


lgb_reg=LGBMRegressor()
random_lgb=RandomizedSearchCV(lgb_reg,params,n_iter=8,scoring='neg_root_mean_squared_error',cv=3,verbose=1,random_state=42,n_jobs=-1)
random_lgb.fit(s3_d1,y3_d1)
random_lgb.best_params_{'colsample_bytree': 1.0,
'learning_rate': 0.1,
'max_depth': 11,
'min_child_samples': 300,
'n_estimators': 800}

Here is the code for hyperparameter tuning for my meta model

params={'max_depth':[3,5,7,9,11],
'learning_rate':[0.1,0.01,0.03,0.05],
'colsample_bytree':[0.7,0.8,0.9,1.0],
'n_estimators':[300,500,800,1200],
'min_child_samples':[50,100,200,300,500]}


lgb_reg=LGBMRegressor()
random_lgb=RandomizedSearchCV(lgb_reg,params,n_iter=8,scoring='neg_root_mean_squared_error',cv=3,verbose=24,random_state=5,n_jobs=-1)
random_lgb.fit(X_train_d2_pred,y_train_d2)
random_lgb.best_params_{'colsample_bytree': 0.9,
'learning_rate': 0.05,
'max_depth': 7,
'min_child_samples': 50,
'n_estimators': 800}

6) Simple MLP Model

Here I am building my first neural network model

model_3=Sequential()
model_3.add(Dense(256,activation='relu',input_shape=(X_train.shape[1],)))
model_3.add(Dense(128,activation='relu'))
model_3.add(Dense(64,activation='relu'))
model_3.add(Dense(32,activation='relu'))
model_3.add(Dense(1))
model_3.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.1),loss=rmse)
model_3.fit(X_train,y_train,epochs=10,validation_data=(X_test,y_test),batch_size=int(X_train.shape[0]/10))Epoch 1/10
11/11 [==============================] - 57s 5s/step - loss: 1200632.2554 - val_loss: 3.3051
Epoch 2/10
11/11 [==============================] - 55s 5s/step - loss: 2.6724 - val_loss: 2.3621
Epoch 3/10
11/11 [==============================] - 55s 5s/step - loss: 2.3916 - val_loss: 2.1868
Epoch 4/10
11/11 [==============================] - 55s 5s/step - loss: 2.1294 - val_loss: 2.0849
Epoch 5/10
11/11 [==============================] - 55s 5s/step - loss: 2.0988 - val_loss: 2.0801
Epoch 6/10
11/11 [==============================] - 55s 5s/step - loss: 2.0969 - val_loss: 2.1468
Epoch 7/10
11/11 [==============================] - 55s 5s/step - loss: 2.1280 - val_loss: 2.0903
Epoch 8/10
11/11 [==============================] - 55s 5s/step - loss: 2.1155 - val_loss: 2.1396
Epoch 9/10
11/11 [==============================] - 55s 5s/step - loss: 2.1278 - val_loss: 2.0815
Epoch 10/10
11/11 [==============================] - 55s 5s/step - loss: 2.0806 - val_loss: 2.0793

11) Test Result Comparison Of All Models

Here we can see that LGBM Model performs the best out of all the models that we have experimented with.

12) Deployment of ML Models using Flask on Google Colab

As the final step I have deployed my model using flask on google colab.One more important thing to note while deploying the model through colab is to use ngrok as it makes the ip public as colab is a virtual machine.Here I am attaching the video link of my deployed model.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store