SQLによる時系列分析


環境
データベース:PostgreSQL ver.15.1.1
実行環境：pgAdmin4-6.19 

グラフについては、pgAdminにグラフを作成する機能があるので、そちらを使用。
元々PythonやExcelで視覚化を試みていたが、pgAdminではGUI上で簡単に生成してくれることに気づいた。

## 1.分析前の基礎知識

### 1-1. 日付、日時、時間の操作
日付と日時はデータソースにより、様々なフォーマットがあるため、変換して揃える必要がある。

#### 1-1-1.タイムゾーン
世界中には様々なタイムゾーンがあるため、それを無視して集計などすると時間がズレて正確な分析ができないことがある。例えば、売上の比較では時間のズレにより、売上を過小評価したり、過大評価してしまう。

多くのデータベースでは、協定世界時（UTC）が基準となっている。

タイムゾーン情報は以下のクエリで取得可能
```sql
SELECT * FROM pg_timezone_names
```

![スクリーンショット 2023-02-01 11.31.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/695bc6ca-4a42-8951-81d1-11e45eaa3ea0.png)


```sql
SELECT COUNT(*) FROM pg_timezone_names
```
596もあるらしい。
![スクリーンショット 2023-01-31 15.21.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/b7fcf28a-03cc-ca2f-8be2-9cecbea05b6a.png)

タイムゾーンを試しにpstに変換してみる。
```sql
SELECT '2023-01-01 00:00:00' at time zone 'pst'
```
時間が17時間早くなっているのが確認できる。
　![スクリーンショット 2023-01-31 15.28.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/56883aa1-0526-010a-d608-000e16d503ba.png)


#### 1-1-2. 日付とタイムスタンプのフォーマット

以下クエリで様々な時間情報を取得できる。

**DATEを取得**
```sql
SELECT current_date
```

![スクリーンショット 2023-01-31 15.36.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/98fc3466-2d1a-65cc-db53-be1784b4da28.png)


**TIMESTAMPを取得**
```sql
SELECT current_timestamp
```

![スクリーンショット 2023-01-31 15.40.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/1e2f2dbd-adcc-9ad4-da7a-8f25a059a49a.png)


#### 日時の変換

```sql
SELECT date_trunc('month', '2023-01-01 12:33:35'::timestamp)
```
![スクリーンショット 2023-01-31 15.52.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/c09cf1b1-e98a-f24f-4c7d-c2f5b9f9fff5.png)

date_part関数で年月日の値を取得できる。
```sql
select date_part('day', current_timestamp)
```
1/31に実行した場合
![スクリーンショット 2023-01-31 15.54.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/7fd4e7ef-ac51-0186-731d-834d837fa0fa.png)
例えば、'day'を'month'に変えれば月が、'year'にすれば年の部分のみが取得できる。

#### 日付の計算

単純にプラスやマイナスで計算できる。
```sql
SELECT date('2023-02-27') - date('2023-02-22') AS days
```
![スクリーンショット 2023-01-31 15.56.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/5b24b1c0-8657-3c03-0bae-d60dff6d0f65.png)



## 2.実際に時系列分析を実施

#### データについて
データ概要：米国小売業売上高データ
テーブル名：sales_retail
データ数：22620レコード


テーブル作成とデータインポート
```sql
DROP table if exists retail_sales;
CREATE table retail_sales
(
sales_month date
,naics_code varchar
,kind_of_business varchar
,reason_for_null varchar
,sales decimal
)

COPY retail_sales 
FROM '/Users/パス/ファイル名' 
DELIMITER ','
CSV HEADER

SELECT * FROM retail_sales
```
クエリ結果についてはレコード数が多いため、一部のみ表示。
kind_of_businessは商品カテゴリを表している。
![スクリーンショット 2023-01-31 16.02.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/47711d7a-f4e2-8b74-9d96-c094096b94e5.png)



#### 2-1.単純なトレンドを確認

**毎年の小売および食品サービス売上高のトレンド(単位：ドル)**
試しに食品関連の売上データを見てみる。

```sql
SELECT 
    date_part('year', sales_month) as sales_year
,   SUM(sales) AS sales
FROM retail_sales
WHERE kind_of_business = 'Retail and food services sales, total'
GROUP BY sales_year
ORDER BY sales_year
```
![スクリーンショット 2023-01-31 21.20.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/2a55491a-55a3-eced-f573-4129cef67542.png)

![スクリーンショット 2023-02-01 9.15.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/1efe52b0-c582-b580-9fb2-120784342f9a.png)
本来はインフレ率によって調整をしなければならないが、今回は実施していない。
グラフを見ると安定して増加しているのが見て取れる。2020年はパンデミックが始まった年だが、食品は生活必需品のため大きな影響を受けていないと思われる。




### 2-2.移動計算

**概要：**
データを平滑化するために複数の期間を考慮。
株式市場、マクロ経済トレンド、視聴率などに適用。
計算方法は移動平均が最も一般的。


**アクティブユーザーの測定**
**・DAU:Daily Actibe Users**
日間アクティブユーザー数で分析。ピーク時の時間ごと、分ごとのユーザー情報が必要。
**・MAU:Monthly Active Users**
月間アクティブユーザー数を分析。アプリケーショやサービスの相対的なサイズを見積もるのに使用できる。
曜日ごとにユーザー利用頻度が異なる場合など、非定期的パターンを測定するのに役立つ。
**・WAU:Weekly Active Users**
週間アクティブユーザー数を分析。DAUとMAUの中間的。


**12ヶ月のウィンドウを使用した移動年間売上高**
移動平均が必要なのでAVG関数を使っている。
自己結合によって作ったt2テーブルから返されるレコードのCOUNTを入れて、各行を12のデータポイントで平均している。
```sql
SELECT 
    t1.sales_month
,   t1.sales
,   AVG(t2.sales) AS moving_avg
,   COUNT(t2.sales) AS records_count
FROM retail_sales AS t1
JOIN retail_sales AS t2 ON(
        t1.kind_of_business = t2.kind_of_business
	AND t2.sales_month BETWEEN t1.sales_month - interval '11 months'
	AND t1.sales_month
	AND t2.kind_of_business = 'Women''s clothing stores'
)
WHERE t1.kind_of_business = 'Women''s clothing stores'
  AND t1.sales_month >= '1993-01-01'
GROUP BY 
    t1.sales_month
,   t1.sales
ORDER BY
    t1.sales_month
,   t1.sales
```
![スクリーンショット 2023-02-01 9.14.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/73276e81-d7fb-8e88-ee85-9c700c48807a.png)



### 2-3.季節性の分析

**書籍の対月比売上を調査**
前年比や前月比やといったある期間を過去の期間と比較する場合、時系列から前の値を返すlag関数が使える。
lagはそのまま使用するとデータ全体を振り返るので、PARTITION BY句+ORDER BY句で指定をする必要がある。


```sql
SELECT 
    kind_of_business
,   sales_month
,   sales
,   ROUND((sales / LAG(sales) OVER (PARTITION BY kind_of_business 
							  ORDER BY sales_month) -1) * 100 , 3) AS 対前月比
FROM retail_sales
WHERE kind_of_business = 'Book stores'
  AND sales_month >= '2018-01-01'
```

![スクリーンショット 2023-02-01 11.27.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/89f1c720-15cb-3e94-6ab9-2867088da7ae.png)
8月の売上が一気に増えているが、米国は9月始業のため、教科書などがよく売れていると思われる。



**書籍の前年同月比売上を調査**
absolute_diffとして、前年同月からの増加分を算出。
lagを使用し、売上/前年同月売上で前年同月比を算出。
そのままだと小数点以下が長々と続いたのでroundで桁数を指定。
```sql
SELECT
    sales_month
,   sales
,   sales - lag(sales) OVER (PARTITION BY date_part('month', sales_month)
							 ORDER BY sales_month) AS absolute_diff
,   ROUND((sales / lag(sales) OVER (PARTITION BY date_part('month', sales_month)
							 ORDER BY sales_month)-1) * 100 , 3) AS 前年同月比
FROM retail_sales
WHERE kind_of_business = 'Book stores'
```

　![スクリーンショット 2023-02-01 11.19.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/d2b55120-9244-3f14-d2c6-748794c22239.png)



補足：ウィンドウ関数
PARTITION BYなどのウィンドウ関数は、データを何らかの基準に従ってグループ分けする関数。GROUP BYもPARTITION BYもテーブルを指定されたキーで分割するが、違いは、GROUP BYは分割後に集約して１行にまとめる操作が入るだけ。
ウィンドウ関数の詳しい解説は下記。
[window関数を使いこなす 〜分析のためのSQL〜](https://qiita.com/HiromuMasuda0228/items/0b20d461f1a80bd30cfc)
[SQL PARTITION BYの基本と効率的に集計する便利な方法](https://zukucode.com/2017/08/sql-over-partition-by.html)

**形を変えてみる**
```sql
SELECT
    date_part('month', sales_month) AS month_number
,   to_char(sales_month, 'Month') AS month_name
,   max(CASE WHEN date_part('year', sales_month) = 2018 
	         THEN sales
	         END) AS sales_2018
,   max(CASE WHEN date_part('year', sales_month) = 2019 
	         THEN sales
	         END) AS sales_2019
,   max(CASE WHEN date_part('year', sales_month) = 2020
	         THEN sales
	         END) AS sales_2020
FROM retail_sales
WHERE kind_of_business = 'Book stores'
  AND sales_month BETWEEN '2018-01-01' AND '2020-12-01'
GROUP BY
    month_number
,   month_name
```
![スクリーンショット 2023-02-01 11.28.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/dac3d73f-d176-7fd6-fe00-05a886b45598.png)


![スクリーンショット 2023-02-01 11.29.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/496614/fa8c200f-14e3-f0f8-23ea-2d9dc953e414.png)
どの年も1月、8月、12月で売上が伸びているのが分かる。


参考図書：[SQLではじめるデータ分析](https://www.oreilly.co.jp/books/9784814400201/)
学習用に、クエリは一部改変してます。