Rでデータの整形(列のデータ型確認、列の抽出、列名の変更、列の型変換)
概要
ローデータから分析対象とする変数のみ抽出し(個人情報など、保持すべきでない変数を削除するなど)、情報を失わない範囲で分析するためのデータセットを作る。分析プロジェクトにおけるローデータと同じ量の情報を持つ、整形された(扱いやすい)データセットを作るのである。 この後のデータクレンジング以降で、データの加工方法を変更するなどで手戻りが発生することもある。その際ローデータの読み込みまで戻るのは大変なので、ローデータを同じ情報を持つ、整形された状態のデータを作っておくのが重要である。データクレンジングで手戻りが発生しても、ここで整形したデータセットまで戻ればいい。
dtplyrについて
本記事ではdata.table(例:fread()で読み込んだcustomer.dt)の操作にライブラリ{dtplyr}を使う。lazy_dt()でdata.tableをラップし、select, mutate, renameなどの関数で列の抽出・型変換を行う。library(dtplyr)を読み込んでおくこと。
変数の型
データの型確認
関数str()を使う。これはデータフレーム、data.table同様に使える関数である。
データフレーム
data.table
基本的なデータの型の種類
boolean
logical: TRUE or FALSE、短縮してT or Fとしても可能
数値
- 整数は
integer
明示的にintegerとして扱うにはx <- 5LのようにLを付ける - 小数を含めると
numeric - bigint相当は
library(bit64)を使うとinteger64として指定できる。fread()で読み込んだ場合、32bit範囲内の整数は通常のinteger型、それを超える大きな整数のみinteger64型として読み込まれる。
文字列の扱い
- 単純な文字列
character - カテゴリカル変数として扱うには
factor - 順序つきカテゴリカル変数として扱う場合
ordered
分析対象とする列の抽出、列名の変更
列の抽出、削除
データフレーム
指定した列を削除する。列名 <- NULLでその列をデータフレームから除去する。
data.table
lazy_dt()でdata.tableをラップし、select()で残す列を指定、最後にas.data.table()で元の形式に戻す。select()で指定しなかった列は削除される。
select()関数名が他のパッケージと重複するので、dtplyr::select()と指定するのが安全である。
列名の変更
データフレーム
names()で列名のベクトルを取得し、条件に合う要素を新列名で置き換える。
data.table
rename(新列名 = 旧列名)で列名を変更する。複数列の変更はカンマ区切りで並べる。
列の型変換
データフレーム
as.factor()で文字列列をカテゴリ変数に、as.logical()で条件式の結果をTRUE/FALSEに変換する。
customer.df$reg_store<- as.factor(customer.df$reg_store)
customer.df$older <- as.logical(customer.df$age>40)
data.table
mutate(列名 = 変換処理の関数(列名))で列を上書きする。$による直接代入と同等の結果になる。
customer.dt %>%
lazy_dt() %>%
mutate(x = 変換処理の関数(x)) %>%
as.data.table()
# customer.dt$x <- 変換処理の関数(customer.dt$x) と同じ
列の型変換の細かいTips
欠損値処理、「NULL」をNAに変換
文字列として「NULL」が入っている場合、Rの欠損値NAに置き換える。[条件]で条件に合う要素のみを指定して代入する。
factor型
factor型への変換と、factor列を数値に戻す
R 4.0.0以降ではread.table()などはstringsAsFactors=FALSEがデフォルトのため、文字列列はcharacter型として読み込まれる。factorにしたい場合はas.factor()で明示的に変換する。
数値に見える文字列列を数値に変換する場合、列がcharacter型ならas.numeric()で直接変換できる。列がfactor型の場合(過去のコードやstringsAsFactors=TRUEで読み込んだデータ)は、as.numeric()だけだと水準のコードが返るため、一旦as.character()にしてからas.numeric()する。
複数のデータフレーム間でfactorの水準(レベル)を統一する
データフレームが変われば同じ意味(都道府県など)のfactor変数でも水準が変わるため、まったく別の変数扱いになってしまう。分析で使う際にはそれらを合わせておく必要がある。
データフレームdf1の列prefecture(factor型)の水準をdf2の列prefectureの水準に合わせる。factor(ベクトル, levels=水準のベクトル)で水準の順序と種類を明示的に指定する。levels(df2$prefecture)でdf2の水準を取得し、それをdf1に適用する。
日付、日時
日付はDate型。as.Date()関数を使う。日時はPOSIXctがスカラなので分かりやすい(エポック秒のスカラ)。POSIXltはリストで要素が複数あるのでややこしい。
as.POSIXct(文字列, format=フォーマット)で文字列を日時型に変換する。formatには%Y(年4桁)、%m(月)、%d(日)、%H(時)、%M(分)、%S(秒)などを指定する。
customer.df$reg_date <- as.POSIXct(customer.df$reg_date, format = '%Y/%m/%d')
customer.df$latest_datetime <- as.POSIXct(customer.df$latest_datetime, format='%Y/%m/%d %H:%M:%S')
※ただし遅いので速さを求めるならライブラリ{lubridate}を使う手もある。 https://qiita.com/hoxo_m/items/6f18b163946f6f41deca ライブラリ{data.table}のIDate(日付)やITime(時刻)形式も高速。この形式はdata.tableの中でない普通のベクトルやデータフレームの中でも使える。
日付
as.IDate()で日付型に変換する。ISO形式(ハイフン区切り)ならformat不要。data.table内では:=で列を更新する。
as.IDate('2020-01-01') # ハイフン区切りならそのまま
as.IDate('20200101', format="%Y%m%d") # フォーマットも指定できる
customer.df$reg_date <- as.IDate(customer.df$reg_date, format = '%Y/%m/%d') # データフレームで扱う場合
customer.dt[,reg_date:=as.IDate(reg_date, format = '%Y/%m/%d')] # data.tableの中で扱う場合
時刻
as.ITime()で時刻型に変換する。コロン区切りならformat不要。時分のみの入力も受け付ける。
as.ITime('09:23:15') # コロン区切りならそのまま
as.ITime('09:23') # 時分のみでもOK
as.ITime('092315', format="%H%M%S") # フォーマットも指定できる
customer.df$reg_time <- as.ITime(customer.df$reg_time, format = '%H%M%S') # データフレームで扱う場合
customer.dt[,reg_time:=as.ITime(reg_time, format = '%H%M%S')] # data.tableの中で扱う場合
日付と時刻を合わせた日時形式は存在しない。その場合はPOSIXct形式を使う。IDateとITimeを足し合わせることでPOSIXctの日時を生成できる。
Rのdata.table形式で日時データを扱う際はPOSIXltを使わない。
データフレームで数字列を日時に変換する。
このようなデータに対して、以下の3通りの方法で日時データに変換する。strptime()は文字列をパースしてPOSIXltを返す。as.character()で数値列を文字列にしておく必要がある。
strptime()のみ実行(結果はPOSIXlt)strptime()してからas.POSIXlt()(明示的にPOSIXlt化)strptime()してからas.POSIXct()(スカラのPOSIXctに変換、推奨)
> x.df$date2 <- strptime(as.character(x.df$date), format="%Y%m%d")
> x.df$date3 <- as.POSIXlt(strptime(as.character(x.df$date), format="%Y%m%d"))
> x.df$date4 <- as.POSIXct(strptime(as.character(x.df$date), format="%Y%m%d"))
>
> head(x.df)
date date2 date3 date4
1 20151122 2015-11-22 2015-11-22 2015-11-22
2 20151118 2015-11-18 2015-11-18 2015-11-18
3 20151123 2015-11-23 2015-11-23 2015-11-23
4 20150330 2015-03-30 2015-03-30 2015-03-30
5 20151106 2015-11-06 2015-11-06 2015-11-06
6 20150801 2015-08-01 2015-08-01 2015-08-01
strptime()はPOSIXltを返すのでdate2とdate3は同じだが比較のため。
POSIXltもPOSIXctも日時データだが、POSIXltでは内部で日時をリスト型変数として、POSIXctは数値型変数として格納している。
これに対してdata.tableでは
> x.dt %>%
lazy_dt() %>%
mutate(
date2 = strptime(as.character(date), format="%Y%m%d"),
date3 = as.POSIXlt(strptime(as.character(date), format="%Y%m%d")),
date4 = as.POSIXct(strptime(as.character(date), format="%Y%m%d"))
) %>%
as.data.table() -> x.dt
警告メッセージ:
1: `[.data.table`(`_dt`, , `:=`(date2, strptime(as.character(date), で:
Supplied 11 items to be assigned to 344 items of column 'date2' (recycled leaving remainder of 3 items).
2: `[.data.table`(`_dt`, , `:=`(date3, as.POSIXlt(strptime(as.character(date), で:
Supplied 11 items to be assigned to 344 items of column 'date3' (recycled leaving remainder of 3 items).
>
> head(x.dt)
date date2 date3 date4
1: 20151122 0,0,0,0,0,0, 0,0,0,0,0,0, 2015-11-22
2: 20151118 0,0,0,0,0,0, 0,0,0,0,0,0, 2015-11-18
3: 20151123 0,0,0,0,0,0, 0,0,0,0,0,0, 2015-11-23
4: 20150330 22,18,23,30, 6, 1, 22,18,23,30, 6, 1, 2015-03-30
5: 20151106 10,10,10, 2,10, 7, 10,10,10, 2,10, 7, 2015-11-06
6: 20150801 115,115,115,115,115,115, 115,115,115,115,115,115, 2015-08-01
変な警告とともにPOSIXlt形式をそのままリストとして扱ってしまう。POSIXlt形式は内部ではリストなのだが、日付としてのラッパーが外れてしまうのである。date2とdate3が壊れ、date4(POSIXct)だけ正しく表示されている。 ということでdata.table内ではPOSIXltは扱えない。気を付けよう。
factor型に順序(Sでいうところのordered型にする)や基準となる水準を与える
factor型には順序を指定することができる。 また順序があってもなくても、回帰係数などの基準となる水準を指定することができる。 Rでは回帰分析の際、factor型の場合には自動でダミー変数を作って係数を推定するが、その係数は先頭の水準をベースとして、それに対する相対的な効果を表している**(水準==aの効果をゼロとした場合のに対して、水準==bの効果がどの程度か)。**特定の水準を基準として係数の大きさやp値を見たい場合には、先頭の水準を変更することによってそれが可能になる。
- 先頭の水準を指定する:
relevel(変更前のfactor型変数名, ref = '先頭の水準のラベル') - 順序全体を指定する:
factor(変更前のfactor型変数名, levels = ラベルを順番に並べたベクトル)
データフレーム
relevel()で回帰分析の基準水準(参照カテゴリ)を変更する。factor(..., levels=c(...))で水準の並び順を指定し、順序付きカテゴリとして扱う。
# category == 'news'を基準にする
x.df$category <- relevel(x.df$category, ref = 'news')
# 'small', 'medium', 'large'の順に並べる
x.df$size <- factor(x.df$size, levels = c('small', 'medium', 'large'))
data.table
x.dt %>%
lazy_dt() %>%
mutate(
# category == 'news'を基準にする
category = relevel(category, ref = 'news'),
# 'small', 'medium', 'large'の順に並べる
size = factor(size, levels = c('small', 'medium', 'large'))
) %>%
as.data.table()
# (参考)添字を使う場合
x.dt[, category := relevel(category, ref = 'news')]
x.dt[, size := factor(size, levels = c('small', 'medium', 'large'))]
分析対象とする列の抽出、列名の変更、列の型変換をdata.tableでまとめて行う
処理が定型的なのでdtplyrのlazy_dt()とパイプを使ってまとめて行うといい。流れは「必要な列だけ残す → 列名を英語などに統一 → 各列を適切な型に変換」の順。
customer.dt %>%
lazy_dt() %>%
# 列の抽出:selectで指定した列だけ残す
dtplyr::select(
`顧客ID`,
`メルマガ登録`,
`登録店舗`,
`登録日時`,
`性別`,
`生年月日`
) %>%
# 列名の変更:日本語列名を英語のスネークケースに
rename(
customer_id = `顧客ID`,
send_mail_magazine = `メルマガ登録`,
reg_store = `登録店舗`,
reg_datetime = `登録日時`,
sex = `性別`,
birthday = `生年月日`
) %>%
# 型変換:across(列名, 関数)で複数列に同じ変換を一括適用
mutate(
across(customer_id, as.integer64), # 32bit超のIDはinteger64型に
across(send_mail_magazine, ~ .x == 1), # 0/1の2値変数をlogical型に(~ .xは無名関数、.xが列の値)
across(c(reg_store, sex), as.factor), # カテゴリ列をfactor型に
across(reg_datetime, ~ as.POSIXct(.x, format='%Y/%m/%d %H:%M:%S')), # 日時をPOSIXct型に
across(birthday, ~ as.Date(.x, format = '%Y/%m/%d')) # 日付をDate型に
) %>%
as.data.table() -> customer.dt
select()は他パッケージと衝突することがあるので、dtplyr::select()と指定するのが安全である。列名を指定する際、スペースや日本語を含む列名はバッククォート「」で囲む。
日時データはPOSIXctを使う。POSIXltを使うとデータが壊れるので絶対に使わないこと。
型変換はmutate(across(列名, 関数))が便利。across()`の第1引数では列名のベクトル以外に、tidyselectのヘルパーで列を指定できる。
- 前方一致:
starts_with('date_')でdate_で始まる列を選択 - 後方一致:
ends_with('_id')で_idで終わる列を選択 - 部分一致:
contains('_t_')で_t_を含む列を選択 - 正規表現一致:
matches('^type[0-9]')でtypeの後に数字が続く列を選択
列の数が1個であればmutate()関数でもいいのだが、
customer.dt %>%
lazy_dt() %>%
rename(
:
) %>%
mutate(
customer_id = as.integer64(customer_id),
send_mail_magazine = send_mail_magazine == 1,
reg_store = as.factor(reg_store),
reg_datetime = as.POSIXct(reg_datetime, format='%Y/%m/%d %H:%M:%S'),
sex = as.factor(sex),
birthday = as.Date(birthday, format = '%Y/%m/%d')
) %>%
as.data.table() -> customer.dt
同様の処理をする列が複数になるとコピペの嵐になる(処理の内容を1箇所書き換えるときに列の数だけ書き換える必要が出てくる)ので、across()を使うのが洗練されたやり方である。処理の内容によってはacross()とmutate()を併用するのもいい。
マスタなどで全列factorに変換する場合、across(everything(), as.factor)で全列に一括適用できる。