PythonとGridDBを用いたWebトラフィックの調査と予測

ここ数年、時系列データベースのカテゴリが最も急速に成長しています。既存の技術分野と新興の技術分野の両方で、時系列データの作成量が増加しているのです。

一定時間内に行われるセッションの量をWebトラフィックと呼び、時間帯や曜日などによって大きく変化します。プラットフォームが処理できるWebトラフィックの量は、プラットフォームをホストするサーバーのサイズによって決定されます。

過去の訪問者数データや過去のWebトラフィックデータに基づいて、多くのサーバーを動的に割り当てることができます。つまり、過去のデータに基づいてセッションやウェブトラフィックの量を分析し、予測することがデータサイエンスの課題なのです。

チュートリアルの概要は以下の通りです。

1.  データセット概要
2.  必要なライブラリのインポート
3.  データセットの読み込み
4.  データの可視化による分析
5.  予測
6.  まとめ

## 前提条件と環境設定

jupyterの全ファイルは、GitHubのリポジトリにあります。

$ git clone --branch web-forecasting https://github.com/griddbnet/Blogs.git

このチュートリアルは、Windows オペレーティングシステム上の Anaconda Navigator (Python バージョン - 3.8.5) で実行されます。チュートリアルを続ける前に、以下のパッケージがインストールされている必要があります。

1.  Pandas
2.  NumPy
3.  re
4.  Matplotlib
5.  Seaborn
6.  griddb_python
7.  fbprophet

これらのパッケージは Conda の仮想環境に `conda install package-name` を使ってインストールすることができます。ターミナルやコマンドプロンプトから直接Pythonを使っている場合は、 `pip install package-name` でインストールできます。

### GridDBのインストール

このチュートリアルでは、データセットをロードする際に、GridDB を使用する方法と、Pandas を使用する方法の 2 種類を取り上げます。Pythonを使用してGridDBにアクセスするためには、以下のパッケージも予めインストールしておく必要があります。

1.  [GridDB Cクライアント][1]
2.  SWIG (Simplified Wrapper and Interface Generator)
3.  [GridDB Pythonクライアント][2]

## 1\. データセット概要

データセットは約145kの時系列で構成されています。これらの時系列はそれぞれ、2015年7月1日から2016年12月31日までの、異なるWikipedia記事の毎日の閲覧回数を表しています。

<https://www.kaggle.com/competitions/web-traffic-time-series-forecasting/data>

## 2\. 必要なライブラリのインポート

```python
%matplotlib inline
import pandas as pd
import numpy as np
import re
import seaborn as sns
from fbprophet import Prophet
import griddb_python as griddb

import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')

import warnings
warnings.filterwarnings("ignore")
```

## 3\. データセットの読み込み

データセットはこちらからダウンロードできます:  
<https://www.kaggle.com/competitions/web-traffic-time-series-forecasting/data?select=train_1.csv.zip>

続けて、データセットをノートブックにロードしてみましょう。

### 3\.a GridDBを利用する

東芝GridDB™は、IoTやビッグデータに最適な高スケーラブルNoSQLデータベースです。GridDBの理念の根幹は、IoTに最適化された汎用性の高いデータストアの提供、高いスケーラビリティ、高性能なチューニング、高い信頼性の確保にあります。

大量のデータを保存する場合、CSVファイルでは面倒なことがあります。GridDBはオープンソースであり、拡張性の高いデータベースであるため、完璧な代替手段として機能します。GridDBは、スケーラブルでインメモリなNo SQLデータベースで、大量のデータを簡単に保存することができます。GridDBを初めて使う場合は、[GridDBへの読み書き][3]のチュートリアルが役に立ちます。

すでにデータベースのセットアップが完了していると仮定して、今度はデータセットをロードするためのSQLクエリをpythonで書いてみましょう。

pandasライブラリが提供するread_sql_query関数は、取得したデータをパンダのデータフレームに変換し、ユーザーが作業しやすいようにします。

```python
sql_statement = ("SELECT * FROM train_1.csv")
df1 = pd.read_sql_query(sql_statement, cont)
```

変数 `cont` には、データが格納されているコンテナ情報が格納されていることに注意してください。`credit_card_dataset` をコンテナの名前に置き換えてください。詳細はチュートリアル[GridDBへの読み書き][3]に記載されています。

IoTやビッグデータのユースケースに関して言えば、GridDBはリレーショナルやNoSQLの領域の他のデータベースの中で明らかに際立っています。全体として、GridDBは高可用性とデータ保持を必要とするミッションクリティカルなアプリケーションのために、複数の信頼性機能を提供しています。

### 3\.b pandasのread_csvを使用する

また、Pandasの `read_csv` 関数を使用してデータを読み込むこともできます。どちらの方法を使っても、データはpandasのdataframeの形で読み込まれるので、上記のどちらの方法も同じ出力になります。

```python
df = pd.read_csv('train_1.csv', parse_dates=True)
```

```python
df.head()
```

| |Page|2015-07-01|2015-07-02|2015-07-03|2015-07-04|2015-07-05|2015-07-06|2015-07-07|2015-07-08|2015-07-09|...|2016-12-22|2016-12-23|2016-12-24|2016-12-25|2016-12-26|2016-12-27|2016-12-28|2016-12-29|2016-12-30|2016-12-31|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|          0|2NE1_zh.wikipedia.org_all-access_spider|18.0|11.0|5.0|13.0|14.0|9.0|9.0|22.0|26.0|...|32.0|63.0|15.0|26.0|14.0|20.0|22.0|19.0|18.0|20.0|
|          1|2PM_zh.wikipedia.org_all-access_spider|11.0|14.0|15.0|18.0|11.0|13.0|22.0|11.0|10.0|...|17.0|42.0|28.0|15.0|9.0|30.0|52.0|45.0|26.0|20.0|
|          2|3C_zh.wikipedia.org_all-access_spider|1.0|0.0|1.0|1.0|0.0|4.0|0.0|3.0|4.0|...|3.0|1.0|1.0|7.0|4.0|4.0|6.0|3.0|4.0|17.0|
|          3|4minute_zh.wikipedia.org_all-access_spider|35.0|13.0|10.0|94.0|4.0|26.0|14.0|9.0|11.0|...|32.0|10.0|26.0|27.0|16.0|11.0|17.0|19.0|10.0|11.0|
|          4|52_Hz_I_Love_You_zh.wikipedia.org_all-access_s...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|...|48.0|9.0|25.0|13.0|3.0|11.0|27.0|13.0|36.0|10.0|
  
5 rows × 551 columns

### 4\. データビジュアライゼーションで分析する

トラフィックはページ言語に影響されるのか？

Wikipediaで使われている様々な言語がデータセットにどのような影響を与えるかは、調べると面白いかもしれないことの一つです。素直な正規表現を使って、wikipediaのURLに含まれる言語コードを検索してみます。

```python
train_1 = df
```

```python
def get_language(page):
    res = re.search('[a-z][a-z].wikipedia.org',page)
    if res:
        return res[0][0:2]
    return 'na'

train_1['lang'] = train_1.Page.map(get_language)

from collections import Counter

print(Counter(train_1.lang))
```

    Counter({'en': 24108, 'ja': 20431, 'de': 18547, 'na': 17855, 'fr': 17802, 'zh': 17229, 'ru': 15022, 'es': 14069})
    

```python
lang_sets = {}
lang_sets['en'] = train_1[train_1.lang=='en'].iloc[:,0:-1]
lang_sets['ja'] = train_1[train_1.lang=='ja'].iloc[:,0:-1]
lang_sets['de'] = train_1[train_1.lang=='de'].iloc[:,0:-1]
lang_sets['na'] = train_1[train_1.lang=='na'].iloc[:,0:-1]
lang_sets['fr'] = train_1[train_1.lang=='fr'].iloc[:,0:-1]
lang_sets['zh'] = train_1[train_1.lang=='zh'].iloc[:,0:-1]
lang_sets['ru'] = train_1[train_1.lang=='ru'].iloc[:,0:-1]
lang_sets['es'] = train_1[train_1.lang=='es'].iloc[:,0:-1]

sums = {}
for key in lang_sets:
    sums[key] = lang_sets[key].iloc[:,1:].sum(axis=0) / lang_sets[key].shape[0]
```

では、次に総再生回数は時間とともにどのように変化するのでしょうか。異なるセットをすべて同じプロットで表示してみます。

```python
days = [r for r in range(sums['en'].shape[0])]

fig = plt.figure(1,figsize=[13,8])
plt.ylabel('Views per Page')
plt.xlabel('Day')
plt.title('Pages in Different Languages')
labels={'en':'English','ja':'Japanese','de':'German',
        'na':'Media','fr':'French','zh':'Chinese',
        'ru':'Russian','es':'Spanish'
       }

for key in sums:
    plt.plot(days,sums[key],label = labels[key] )
    
plt.legend()
plt.show()
```

[<img src="https://griddb.net/en/wp-content/uploads/2022/11/output_27_0.png" alt="" width="708" height="420" class="aligncenter size-full wp-image-28931" />][4]

英語は1ページあたりの閲覧数が非常に多く、これはWikipediaが米国を拠点とするサイトであることから予想されます。

```python
df1 = df.T
df1 = df1.reset_index()
```

```python
df1.head()
```
  
| |Date|2NE1_zh.wikipedia.org_all-access_spider|2PM_zh.wikipedia.org_all-access_spider|3C_zh.wikipedia.org_all-access_spider|4minute_zh.wikipedia.org_all-access_spider|52_Hz_I_Love_You_zh.wikipedia.org_all-access_spider|5566_zh.wikipedia.org_all-access_spider|91Days_zh.wikipedia.org_all-access_spider|A'N'D_zh.wikipedia.org_all-access_spider|AKB48_zh.wikipedia.org_all-access_spider|...|Drake_(músico)_es.wikipedia.org_all-access_spider|Skam_(serie_de_televisión)_es.wikipedia.org_all-access_spider|Legión_(serie_de_televisión)_es.wikipedia.org_all-access_spider|Doble_tentación_es.wikipedia.org_all-access_spider|Mi_adorable_maldición_es.wikipedia.org_all-access_spider|Underworld_(serie_de_películas)_es.wikipedia.org_all-access_spider|Resident_Evil:_Capítulo_Final_es.wikipedia.org_all-access_spider|Enamorándome_de_Ramón_es.wikipedia.org_all-access_spider|Hasta_el_último_hombre_es.wikipedia.org_all-access_spider|Francisco_el_matemático_(serie_de_televisión_de_2017)_es.wikipedia.org_all-access_spider|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|          1|2015-07-01|18.0|11.0|1.0|35.0|NaN|12.0|NaN|118.0|5.0|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|
|          2|2015-07-02|11.0|14.0|0.0|13.0|NaN|7.0|NaN|26.0|23.0|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|
|          3|2015-07-03|5.0|15.0|1.0|10.0|NaN|4.0|NaN|30.0|14.0|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|
|          4|2015-07-04|13.0|18.0|1.0|94.0|NaN|5.0|NaN|24.0|12.0|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|
|          5|2015-07-05|14.0|11.0|0.0|4.0|NaN|20.0|NaN|29.0|9.0|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|

5 rows × 145064 columns


```python
df1=df1[:550]
```

```python
column_header = df1.iloc[0,:].values

df1.columns = column_header
    
df1 = df1.drop(0, axis = 0)
df1 = df1.rename(columns = {"Page" : "Date"})
```

```python
df1["Date"] = pd.to_datetime(df1["Date"], format='%Y-%m-%d')
df1 = df1.set_index("Date")
```

```python
# Finding number of access types and agents
access_types = []
agents = []
for column in df1.columns:
    access_type = column.split("_")[-2]
    agent = column.split("_")[-1]
    access_types.append(access_type)
    agents.append(agent)
```

```python
# Counting access types
from collections import Counter
access_dict = Counter(access_types)
access_dict
```

    Counter({'all-access': 74315, 'desktop': 34809, 'mobile-web': 35939})
    
```python
access_df = pd.DataFrame({"Access type" : access_dict.keys(),
                          "Number of columns" : access_dict.values()})
access_df
```

| |Access type|Number of columns|
|:----|:----|:----|
|          0|all-access|74315|
|          1|desktop|34809|
|          2|mobile-web|35939|

```python
agents_dict = Counter(agents)
agents_dict
```

    Counter({'spider': 34913, 'all-agents': 110150})

```python
agents_df = pd.DataFrame({"Agent" : agents_dict.keys(),
                          "Number of columns" : agents_dict.values()})
agents_df
```

| |Agent|Number of columns|
|:----|:----|:----|
|          0|spider|34913|
|          1|all-agents|110150|

```python
df1.columns[86543].split("_")[-3:]
"_".join(df1.columns[86543].split("_")[-3:])

projects = []
for column in df1.columns:
    project = column.split("_")[-3] 
    projects.append(project)
```

```python
project_dict = Counter(projects)
project_df = pd.DataFrame({"Project" : project_dict.keys(),
                           "Number of columns" : project_dict.values()})

project_df
```

| |Project|Number of columns|
|:----|:----|:----|
|          0|zh.wikipedia.org|17229|
|          1|fr.wikipedia.org|17802|
|          2|en.wikipedia.org|24108|
|          3|commons.wikimedia.org|10555|
|          4|ru.wikipedia.org|15022|
|          5|www.mediawiki.org|7300|
|          6|de.wikipedia.org|18547|
|          7|ja.wikipedia.org|20431|
|          8|es.wikipedia.org|14069|

```python
def extract_average_views(project):
    required_column_names = [column for column in df1.columns if project in column]
    average_views = df1[required_column_names].sum().mean()
    return average_views

average_views = []
for project in project_df["Project"]:
    average_views.append(extract_average_views(project))
```

```python
project_df["Average views"] = average_views
project_df['Average views'] = project_df['Average views'].astype('int64')
```

```python
project_df
```

| |Project|Number of columns|Average views|
|:----|:----|:----|:----|
|          0|zh.wikipedia.org|17229|184107|
|          1|fr.wikipedia.org|17802|358264|
|          2|en.wikipedia.org|24108|2436898|
|          3|commons.wikimedia.org|10555|99429|
|          4|ru.wikipedia.org|15022|532443|
|          5|www.mediawiki.org|7300|31411|
|          6|de.wikipedia.org|18547|477813|
|          7|ja.wikipedia.org|20431|419523|
|          8|es.wikipedia.org|14069|674546|


```python
project_df_sorted = project_df.sort_values(by = "Average views", ascending = False)
```

```python
plt.figure(figsize = (10,6))
sns.barplot(x = project_df_sorted["Project"], y = project_df_sorted["Average views"])
plt.xticks(rotation = "vertical")
plt.title("Average views per each project")
plt.show()
```

[<img src="https://griddb.net/en/wp-content/uploads/2022/11/output_45_0.png" alt="" width="708" height="420" class="aligncenter size-full wp-image-28931" />][5]

"en.wikipedia.org" の人気ページ

```python
en_wikipedia_org_columns = [column for column in df1.columns if "en.wikipedia.org" in column]

top_pages_en = df1[en_wikipedia_org_columns].mean().sort_values(ascending = False)[0:5]
df1[top_pages_en.index].plot(figsize = (16,9))
```

    <AxesSubplot:xlabel='Date'>

[<img src="https://griddb.net/en/wp-content/uploads/2022/11/output_47_1.png" alt="" width="708" height="420" class="aligncenter size-full wp-image-28931" />][6]

## 5\. 予測

```python
train = df
```

```python
train
```

| |Page|2015-07-01|2015-07-02|2015-07-03|2015-07-04|2015-07-05|2015-07-06|2015-07-07|2015-07-08|2015-07-09|...|2016-12-23|2016-12-24|2016-12-25|2016-12-26|2016-12-27|2016-12-28|2016-12-29|2016-12-30|2016-12-31|lang|
|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|:----|
|          0|2NE1_zh.wikipedia.org_all-access_spider|18.0|11.0|5.0|13.0|14.0|9.0|9.0|22.0|26.0|...|63.0|15.0|26.0|14.0|20.0|22.0|19.0|18.0|20.0|zh|
|          1|2PM_zh.wikipedia.org_all-access_spider|11.0|14.0|15.0|18.0|11.0|13.0|22.0|11.0|10.0|...|42.0|28.0|15.0|9.0|30.0|52.0|45.0|26.0|20.0|zh|
|          2|3C_zh.wikipedia.org_all-access_spider|1.0|0.0|1.0|1.0|0.0|4.0|0.0|3.0|4.0|...|1.0|1.0|7.0|4.0|4.0|6.0|3.0|4.0|17.0|zh|
|          3|4minute_zh.wikipedia.org_all-access_spider|35.0|13.0|10.0|94.0|4.0|26.0|14.0|9.0|11.0|...|10.0|26.0|27.0|16.0|11.0|17.0|19.0|10.0|11.0|zh|
|          4|52_Hz_I_Love_You_zh.wikipedia.org_all-access_s...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|...|9.0|25.0|13.0|3.0|11.0|27.0|13.0|36.0|10.0|zh|
|          ...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|
|          145058|Underworld_(serie_de_películas)_es.wikipedia.o...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|...|NaN|NaN|NaN|13.0|12.0|13.0|3.0|5.0|10.0|es|
|          145059|Resident_Evil:_Capítulo_Final_es.wikipedia.org...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|es|
|          145060|Enamorándome_de_Ramón_es.wikipedia.org_all-acc...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|es|
|          145061|Hasta_el_último_hombre_es.wikipedia.org_all-ac...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|es|
|          145062|Francisco_el_matemático_(serie_de_televisión_d...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|...|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|es|

145063 rows × 552 columns

```python
train=pd.melt(df[list(df.columns[-50:])+['Page']], id_vars='Page', var_name='date', value_name='Visits')
```

```python
list1 = ['lang']
train = train[train.date.isin(list1) == False]
```

```python
train
```

| |Page|date|Visits|
|:----|:----|:----|:----|
|          0|2NE1_zh.wikipedia.org_all-access_spider|2016-11-13|8.0|
|          1|2PM_zh.wikipedia.org_all-access_spider|2016-11-13|11.0|
|          2|3C_zh.wikipedia.org_all-access_spider|2016-11-13|4.0|
|          3|4minute_zh.wikipedia.org_all-access_spider|2016-11-13|13.0|
|          4|52_Hz_I_Love_You_zh.wikipedia.org_all-access_s...|2016-11-13|11.0|
|          ...|...|...|...|
|          7108082|Underworld_(serie_de_películas)_es.wikipedia.o...|2016-12-31|10.0|
|          7108083|Resident_Evil:_Capítulo_Final_es.wikipedia.org...|2016-12-31|NaN|
|          7108084|Enamorándome_de_Ramón_es.wikipedia.org_all-acc...|2016-12-31|NaN|
|          7108085|Hasta_el_último_hombre_es.wikipedia.org_all-ac...|2016-12-31|NaN|
|          7108086|Francisco_el_matemático_(serie_de_televisión_d...|2016-12-31|NaN|

7108087 rows × 3 columns

```python
train['date'] = train['date'].astype('datetime64[ns]')
train['weekend'] = ((train.date.dt.dayofweek) // 5 == 1).astype(float)
median = pd.DataFrame(train.groupby(['Page'])['Visits'].median())
median.columns = ['median']
mean = pd.DataFrame(train.groupby(['Page'])['Visits'].mean())
mean.columns = ['mean']

train = train.set_index('Page').join(mean).join(median)
train.reset_index(drop=False,inplace=True)
train['weekday'] = train['date'].apply(lambda x: x.weekday())

train['year']=train.date.dt.year 
train['month']=train.date.dt.month 
train['day']=train.date.dt.day
```

```python
mean_g = train[['Page','date','Visits']].groupby(['date'])['Visits'].mean()

means =  pd.DataFrame(mean_g).reset_index(drop=False)
means['weekday'] =means['date'].apply(lambda x: x.weekday())

means['Date_str'] = means['date'].apply(lambda x: str(x))

#create new columns year,month,day in the dataframe bysplitting the date string on hyphen and converting them to a list of values and add them under the column names year,month and day
means[['year','month','day']] = pd.DataFrame(means['Date_str'].str.split('-',2).tolist(), columns = ['year','month','day'])

#creating a new dataframe date by splitting the day column into 2 in the means data frame on sapce, to understand these steps look at the subsequent cells to understand how the day column looked before this step
date = pd.DataFrame(means['day'].str.split(' ',2).tolist(), columns = ['day','other'])
means['day'] = date['day']*1
```

```python
means.drop('Date_str',axis = 1, inplace =True)
```

```python
import seaborn as sns
sns.set(font_scale=1) 

date_index = means[['date','Visits']]
date_index = date_index.set_index('date')

prophet = date_index.copy()
prophet.reset_index(drop=False,inplace=True)
prophet.columns = ['ds','y']

m = Prophet()
m.fit(prophet)

future = m.make_future_dataframe(periods=30,freq='D')
forecast = m.predict(future)

fig = m.plot(forecast)
```

    INFO:fbprophet:Disabling yearly seasonality. Run prophet with yearly_seasonality=True to override this.
    INFO:fbprophet:Disabling daily seasonality. Run prophet with daily_seasonality=True to override this.

[<img src="https://griddb.net/en/wp-content/uploads/2022/11/output_57_1.png" alt="" width="708" height="420" class="aligncenter size-full wp-image-28931" />][7]

## 6\. 結論

このチュートリアルでは、PythonとGridDBを使用してWebトラフィックを分析し、予測しました。データのインポート方法として、(1) GridDB と (2) Pandas の2つの方法を検討しました。GridDBは、オープンソースで拡張性が高いため、大規模なデータセットの場合、ノートブックにデータをインポートするための優れた代替手段を提供します。

 [1]: https://github.com/griddb/c_client
 [2]: https://github.com/griddb/python_client
 [3]: https://griddb.net/ja/blog/using-pandas-dataframes-with-griddb/
 [4]: https://griddb.net/en/wp-content/uploads/2022/11/output_27_0.png
 [5]: https://griddb.net/en/wp-content/uploads/2022/11/output_45_0.png
 [6]: https://griddb.net/en/wp-content/uploads/2022/11/output_47_1.png
 [7]: https://griddb.net/en/wp-content/uploads/2022/11/output_57_1.png