Rでデータの整形(列のデータ型確認、列の抽出、列名の変更、列の型変換)

概要

ローデータから分析対象とする変数のみ抽出し(個人情報など、保持すべきでない変数を削除するなど)、情報を失わない範囲で分析するためのデータセットを作る。分析プロジェクトにおけるローデータと同じ量の情報を持つ、整形された(扱いやすい)データセットを作るのである。 この後のデータクレンジング以降で、データの加工方法を変更するなどで手戻りが発生することもある。その際ローデータの読み込みまで戻るのは大変なので、ローデータを同じ情報を持つ、整形された状態のデータを作っておくのが重要である。データクレンジングで手戻りが発生しても、ここで整形したデータセットまで戻ればいい。

本記事ではdata.table(例:fread()で読み込んだcustomer.dt)の操作にライブラリ{dtplyr}を使う。lazy_dt()でdata.tableをラップし、select, mutate, renameなどの関数で列の抽出・型変換を行う。library(dtplyr)を読み込んでおくこと。

関数str()を使う。これはデータフレーム、data.table同様に使える関数である。

str(customer.df)
r
str(customer.dt)
customer.dt |> str()
r

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でその列をデータフレームから除去する。

customer.df$firstname <- NULL
r

lazy_dt()でdata.tableをラップし、select()で残す列を指定、最後にas.data.table()で元の形式に戻す。select()で指定しなかった列は削除される。

customer.dt %>%
  lazy_dt() %>%
  dtplyr::select(列名, ...) %>%
  as.data.table()
r

select()関数名が他のパッケージと重複するので、dtplyr::select()と指定するのが安全である。

names()で列名のベクトルを取得し、条件に合う要素を新列名で置き換える。

names(customer.df)[names(customer.df) == "旧列名"] <- "新列名"
r

rename(新列名 = 旧列名)で列名を変更する。複数列の変更はカンマ区切りで並べる。

customer.dt %>%
  lazy_dt() %>%
  rename(新列名 = 旧列名) %>%
  as.data.table()
r

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)
r

mutate(列名 = 変換処理の関数(列名))で列を上書きする。$による直接代入と同等の結果になる。

customer.dt %>%
  lazy_dt() %>%
  mutate(x = 変換処理の関数(x)) %>%
  as.data.table()
# customer.dt$x <- 変換処理の関数(customer.dt$x) と同じ
r

文字列として「NULL」が入っている場合、Rの欠損値NAに置き換える。[条件]で条件に合う要素のみを指定して代入する。

customer.df$reg_store[customer.df$reg_store=='NULL'] <- NA
r

R 4.0.0以降ではread.table()などはstringsAsFactors=FALSEがデフォルトのため、文字列列はcharacter型として読み込まれる。factorにしたい場合はas.factor()で明示的に変換する。

数値に見える文字列列を数値に変換する場合、列がcharacter型ならas.numeric()で直接変換できる。列がfactor型の場合(過去のコードやstringsAsFactors=TRUEで読み込んだデータ)は、as.numeric()だけだと水準のコードが返るため、一旦as.character()にしてからas.numeric()する

customer.df$customer_grp <- as.numeric(as.character(customer.df$customer_grp))
r

データフレームが変われば同じ意味(都道府県など)のfactor変数でも水準が変わるため、まったく別の変数扱いになってしまう。分析で使う際にはそれらを合わせておく必要がある。 データフレームdf1の列prefecture(factor型)の水準をdf2の列prefectureの水準に合わせる。factor(ベクトル, levels=水準のベクトル)で水準の順序と種類を明示的に指定する。levels(df2$prefecture)でdf2の水準を取得し、それをdf1に適用する。

df1$prefecture <- factor(df1$prefecture, levels=levels(df2$prefecture))
r

日付は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')
r

※ただし遅いので速さを求めるならライブラリ{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の中で扱う場合
r

時刻

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の中で扱う場合
r

日付と時刻を合わせた日時形式は存在しない。その場合はPOSIXct形式を使う。IDateとITimeを足し合わせることでPOSIXctの日時を生成できる。

as.POSIXct('2020-01-01') + as.ITime('09:23')
r

データフレームで数字列を日時に変換する。

> head(x.df)
        date
1   20151122
2   20151118
3   20151123
4   20150330
5   20151106
6   20150801
r

このようなデータに対して、以下の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
r

strptime()はPOSIXltを返すのでdate2とdate3は同じだが比較のため。 POSIXltもPOSIXctも日時データだが、POSIXltでは内部で日時をリスト型変数として、POSIXctは数値型変数として格納している。 これに対してdata.tableでは

> head(x.dt)
         date
  1: 20151122
  2: 20151118
  3: 20151123
  4: 20150330
  5: 20151106
  6: 20150801
r
> 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
r

変な警告とともにPOSIXlt形式をそのままリストとして扱ってしまう。POSIXlt形式は内部ではリストなのだが、日付としてのラッパーが外れてしまうのである。date2とdate3が壊れ、date4(POSIXct)だけ正しく表示されている。 ということでdata.table内ではPOSIXltは扱えない。気を付けよう。

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'))
r
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'))]
r

処理が定型的なので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
r

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
r

同様の処理をする列が複数になるとコピペの嵐になる(処理の内容を1箇所書き換えるときに列の数だけ書き換える必要が出てくる)ので、across()を使うのが洗練されたやり方である。処理の内容によってはacross()mutate()を併用するのもいい。

マスタなどで全列factorに変換する場合、across(everything(), as.factor)で全列に一括適用できる。

x.dt %>%
  lazy_dt() %>%
  mutate(across(everything(), as.factor)) %>%
  as.data.table()
r