今回は番外編です。2023年7月15日にISACA名古屋支部のSR分科会にお邪魔した際、次のようなハンズオンのお題がありました。
※本来は上位30事業者の図示でしたが、ここでは見やすさのために5事業者にしました。
このお題は、tidyverseパッケージを使ってデータを取得・加工・視覚化する好適例なので、ここでWrite-Upする次第です。
最近はライブラリの読み込みに、pacmanが紹介されることが増えました。 p_load()は、既に読み込まれていたら無視し、読み込まれておらず環境にあれば読み込んでくれ、環境になければCRANからダウンロードしてインストール・読み込みを行ってくれる、とても楽勝感の強い関数です。私も最近は専らp_load()を使っています。
arrowは、ダウンロードして作ったデータフレームをApache Feather形式(RやPythonで共通して使えるデータフレームのファイル保存形式)で保存するため、 scalesは、ggplotの作図で軸の表現を簡単に行うために読み込みます。
JPCERT/CCのフィッシングURLリストは、 GitHubのディレクトリに保管されています。
試しに2022年4月の生データのURLを調べると、次のようになっていました。
https://raw.githubusercontent.com/JPCERTCC/phishurl-list/main/2022/202204.csv
これを踏まえ、2022年4月から2023年3月までのダウンロードURLリストを作ります。
base_url <- "https://github.com/JPCERTCC/phishurl-list/raw/main/"
files <-
seq(ymd("2022-04-01"), ymd("2023-03-31"), by="month") |>
strftime("%Y/%Y%m.csv")
urls <- str_c(base_url, files)このうち、以下の部分がコツです。
パイプの左側のseq()は、シーケンスを作るBase R関数です。間隔のbyが柔軟で、「月ごと」といった指定が可能になっています。
## [1] "2022-04-01" "2022-05-01" "2022-06-01" "2022-07-01" "2022-08-01"
## [6] "2022-09-01" "2022-10-01" "2022-11-01" "2022-12-01" "2023-01-01"
## [11] "2023-02-01" "2023-03-01"
上でDate型のベクトルができたので、strftime()を使って文字列型とするときに、 JPCERT/CCのディレクトリ構造に合わせて「YYYY/YYYYMM.csv」となるようにします。
## [1] "2022/202204.csv" "2022/202205.csv" "2022/202206.csv" "2022/202207.csv"
## [5] "2022/202208.csv" "2022/202209.csv" "2022/202210.csv" "2022/202211.csv"
## [9] "2022/202212.csv" "2023/202301.csv" "2023/202302.csv" "2023/202303.csv"
これをstr_c()でbase_urlと連結させたわけです。
先ほど要素を12個もつ文字列ベクトルができたので、これらを read_csv()で読み込みます。read_csv()は、圧縮形式やURL形式にも対応しているのでした。
read_csv()を12回書くのは面倒なので、map_*()関数で一括処理します。 tidyverseでは、for文に相当することはmap_*()関数で済みます。 map_dfrなので、戻り値はデータフレーム(tibble)です。
次にwrite_feather()で、作ったデータフレーム(events)を保存しておきます。保存したデータフレームは、
のようにすることで呼び出せます。
ちなみに現在では、map_dfr()はあまり推奨されていません(Superseded扱い)。これは、map_dfr()が他のmap_*()と内部的に異なる挙動をするためです。
かわりに推奨されている書き方は
と、いちどリストで取得してから縦方向にデータフレームとして結合することなのですが、私自身は新しい書き方に価値を見出せません。論理的な整合性は向上するものの、感覚的な整合性は落ちているように見えるからです。 map_dfr()は、データフレームを返すという点で、動作の内部的挙動はどうあれ map_chr()やmap_dbl()と同じように「結果」が類推できます。
今回、図解には2つ条件がありました。
これらを実現するために、先ほどのデータフレームeventsを加工します。
events_mod <-
events |>
mutate(
date = as_date(date),
week = floor_date(date, unit = "week"),
desc = fct_lump_n(description, n = 5, other_level = "Others")
)floor_date()は切り下げの関数で、単位を週とした場合、デフォルトでは直前の日曜日にまで日付が切り下げられます。
fct_lump_*()はForcatパッケージに含まれる関数で、fct_lump_n()とすると上位n件以外を一塊(lump)にします。ググってみると日本語のサイトで取り上げているものがなかったので、記載することにしました。
ちゃんとできているか、データフレームの先頭10行を出力してみましょう。(フィッシングURLがリンクになるとまずいので、以下では出力から落としています。) 4月1日(金)が3月27日(日)に切り落とされ、また「エポスカード」がOthersになっているので、大丈夫そうです。
| date | description | week | desc |
|---|---|---|---|
| 2022-04-01 | 三菱UFJニコス | 2022-03-27 | 三菱UFJニコス |
| 2022-04-01 | 三菱UFJニコス | 2022-03-27 | 三菱UFJニコス |
| 2022-04-01 | エポスカード | 2022-03-27 | Others |
| 2022-04-01 | Amazon | 2022-03-27 | Amazon |
| 2022-04-01 | エポスカード | 2022-03-27 | Others |
| 2022-04-01 | エポスカード | 2022-03-27 | Others |
| 2022-04-01 | エポスカード | 2022-03-27 | Others |
| 2022-04-01 | エポスカード | 2022-03-27 | Others |
| 2022-04-01 | エポスカード | 2022-03-27 | Others |
| 2022-04-01 | エポスカード | 2022-03-27 | Others |
ここまでくれば、あとはggplotで視覚化するだけです。最低限必要なのは、
です。今回はイベントデータを直接ggplotにもっていきましたので、折れ線グラフのオプションで「stats = “count”」として集計するようにしています。私自身は、先にデータフレーム自体を週次に加工し、 ggplotの負担を小さくすることが多いです(その手法は末尾に掲載しました)。
軸周辺が見づらいので、簡単に手入れをして完成です。
events_mod |>
ggplot(aes(x = week, color = desc)) +
geom_line(stat = "count", alpha = 0.8) +
scale_x_date(date_breaks = "2 weeks",
minor_breaks = "week",
limits = c(ymd("2022-03-25"), ymd("2023-03-28")),
expand = c(0.01, 0.01),
labels = label_date_short(format = c("%Y", "%b.", "%e", "%H"), sep = "\n")
) +
labs(x = "Week", y = "Distinct Count of URLs", color = "Description") +
theme(
legend.position = "bottom",
legend.justification = "center"
)データの取得から加工、視覚化まで、tidyverseを使うと短いコードで、プログラミングっぽさを前面に押し出さずに実現できます。
コード片を1つにまとめておきます。下では、データフレーム上で先に集計する方式を採用しました。
# install.pacakages("pacman")
pacman::p_load(tidyverse, arrow, scales)
base_url <- "https://github.com/JPCERTCC/phishurl-list/raw/main/"
files <-
seq(ymd("2022-04-01"), ymd("2023-03-31"), by="month") |>
strftime("%Y/%Y%m.csv")
urls <- str_c(base_url, files)
events <- map_dfr(urls, read_csv)
write_feather(events, "data/jpcertcc_phishurl_fy22.feather")
events |>
mutate(
date = as_date(date),
week = floor_date(date, unit = "week"),
desc = fct_lump_n(description, n = 5, other_level = "Others")
) |>
group_by(week, desc) |>
tally() |>
ggplot(aes(x = week, y = n, color = desc)) +
geom_line(alpha = 0.8) +
scale_x_date(date_breaks = "2 weeks",
minor_breaks = "week",
limits = c(ymd("2022-03-25"), ymd("2023-03-28")),
expand = c(0.01, 0.01),
labels = label_date_short(format = c("%Y", "%b.", "%e", "%H"), sep = "\n")
) +
labs(x = "Week", y = "Distinct Count of URLs", color = "Description") +
theme(
legend.position = "bottom",
legend.justification = "center"
)