vctrs は class()
や length()
を使う代わりに、 prototype (vec_ptype_show()
) や size (vec_size()
) という概念を持っています。 この vignette では、なぜこれらの代替概念が必要なのかという動機を説明し、それらの定義を型強制とリサイクルルールに結びつけます。
size と prototype は、c()
とrbind()
の最適な動作を考えたときに、特に行列やデータフレームの列を持つデータフレームに触発されて生まれました。
プロトタイプのアイデアは,データをキャプチャすることなく,ベクトルに関連するメタデータをキャプチャすることです.残念ながら、オブジェクトのclass()
はこの目的には不十分です。
class()
には属性が含まれていません。属性が重要な理由は以下の通りです。 例えば,因子のレベルや,POSIXct
のタイムゾーンを格納するために,属性は重要です. 属性を考えずに2つの因子や2つのPOSIXct
を組み合わせることはできません. 属性について考えることなく,2つの因子や2つのPOSIXct
を組み合わせることはできません.
行列の class()
は “matrix”であり,基礎となるベクトルの型や次元は含まれません.
代わりに、vctrs は R のベクトル化された性質を利用して、 prototype というベクトルの 0 観測のスライスを使用します (これは基本的には x[0]
ですが、後で説明するような微妙な点があります)。 これはベクトルのミニチュア版で、すべての属性を含みますが、データは含みません。
便利なことに,既存の Base 関数(例えば,double()
やfactor(levels = c("a", "b"))
など)を用いて,多くのプロトタイプを作成することができます。 vctrsは,RのBase 関数では同等のものがない場合に,いくつかのヘルパー(例えば,new_date()
, new_datetime()
, new_duration()
)を提供します。
vec_ptype()
は既存のオブジェクトからプロトタイプを作成します。しかし,多くのBase ベクトルは,0長の部分集合のための情報のない印刷方法を持っているので,vctrs は,フレンドリーな方法でプロトタイプを印刷する (そして何も返さない) vec_ptype_show()
も提供しています。
vec_ptype_show()`を使うと,Rの基本クラスのプロトタイプを見ることができます.
アトミックベクトルは属性を持たず、基礎となる typeof()
を表示するだけです。
行列や配列のプロトタイプには、基本型と最初の後の次元が含まれています。:
因子のプロトタイプには、そのレベルが含まれます。 レベルは文字ベクトルで、任意の長さにすることができるので、プロトタイプでは単にハッシュを表示しています。 2つの因子のハッシュが等しければ、そのレベルも等しい可能性が高いです。
vec_ptype_show(factor("a"))
#> Prototype: factor<4d52a>
vec_ptype_show(ordered("b"))
#> Prototype: ordered<9b7e3>
vec_ptype_show()
はハッシュだけを表示しますが、プロトタイプオブジェクト自体はすべてのレベルを含んでいます。
Base Rには、日付、日付時刻(POSIXct
)、期間(difftime
)という3つの主要な日付時刻クラスがあります。日付-時刻はタイムゾーンを持ち、期間は単位を持ちます。
データフレームは、最も複雑なプロトタイプを持っています:データフレームのプロトタイプは、各カラムの名前とプロトタイプです。:
vec_ptype_show(data.frame(a = FALSE, b = 1L, c = 2.5, d = "x"))
#> Prototype: data.frame<
#> a: logical
#> b: integer
#> c: double
#> d: factor<bf275>
#> >
データフレームは、それ自体がデータフレームである列を持つことができ、これは「再帰的」な型となります。
vctrs は、vec_ptype_common()
を通じて、強制のための一貫したルールを提供しています。vec_ptype_common()
は以下の不変量を持ちます。
class(vec_ptype_common(x, y))
は class(vec_ptype_common(y, x))
と等しい。
class(vec_ptype_common(x, vec_ptype_common(y, z))
は class(vec_ptype_common(vec_ptype_common(x, y), z))
と等しい。
vec_ptype_common(x, NULL) == vec_ptype(x)
。
つまり、 vec_ptype_common()
は(クラスに関して)可換性と連想性の両方を持ち、同一の要素であるNULL
を持つ、つまり 可換性モノイド なのです。つまり、基本的な実装は非常にシンプルで、オブジェクトのペアの共通の型を順に見つけることで、任意の数のオブジェクトの共通の型を見つけることができます。
vec_ptype()
と同様に、 vec_ptype_common()
を探索する最も簡単な方法は、 vec_ptype_show()
です: 複数の入力が与えられると、それらの共通のプロトタイプを表示します。(言い換えれば、 vec_ptype_common()
でプログラムして、 vec_ptype_show()
で遊ぼう、ということです。)
アトミックベクトルの共通型は、文字に自動的に強制されないことを除けば、base R のルールと非常によく似た方法で計算されます。:
行列や配列は自動的に高次元にブロードキャストされます。:
vec_ptype_show(
array(1, c(0, 1)),
array(1, c(0, 2))
)
#> Prototype: <double[,2]>
#> 0. ( , <double[,1]> ) = <double[,1]>
#> 1. ( <double[,1]> , <double[,2]> ) = <double[,2]>
vec_ptype_show(
array(1, c(0, 1)),
array(1, c(0, 3)),
array(1, c(0, 3, 4)),
array(1, c(0, 3, 4, 5))
)
#> Prototype: <double[,3,4,5]>
#> 0. ( , <double[,1]> ) = <double[,1]>
#> 1. ( <double[,1]> , <double[,3]> ) = <double[,3]>
#> 2. ( <double[,3]> , <double[,3,4]> ) = <double[,3,4]>
#> 3. ( <double[,3,4]> , <double[,3,4,5]> ) = <double[,3,4,5]>
ディメンジョンがvctrsのリサイクルルールに従っているとすると:
因子型 (factor) は、登場する順番にレベルを結合します。
date 型と date-time 型の結合は、date-time 型を生成します。:
vec_ptype_show(new_date(), new_datetime())
#> Prototype: <datetime<local>>
#> 0. ( , <date> ) = <date>
#> 1. ( <date> , <datetime<local>> ) = <datetime<local>>
二つの date 型を結合する場合、タイムゾーンは第1引数を引き継ぎます。
vec_ptype_show(
new_datetime(tzone = "US/Central"),
new_datetime(tzone = "Pacific/Auckland")
)
#> Prototype: <datetime<US/Central>>
#> 0. ( , <datetime<US/Central>> ) = <datetime<US/Central>>
#> 1. ( <datetime<US/Central>> , <datetime<Pacific/Auckland>> ) = <datetime<US/Central>>
ただし、ローカルタイムゾーンの場合は除きます。ローカルの場合、ローカルでない明示的なタイムゾーンが優先されます。:
vec_ptype_show(
new_datetime(tzone = ""),
new_datetime(tzone = ""),
new_datetime(tzone = "Pacific/Auckland")
)
#> Prototype: <datetime<Pacific/Auckland>>
#> 0. ( , <datetime<local>> ) = <datetime<local>>
#> 1. ( <datetime<local>> , <datetime<local>> ) = <datetime<local>>
#> 2. ( <datetime<local>> , <datetime<Pacific/Auckland>> ) = <datetime<Pacific/Auckland>>
2つのデータフレームの共通型は、両データフレームに出現する各列の共通型となります。:
vec_ptype_show(
data.frame(x = FALSE),
data.frame(x = 1L),
data.frame(x = 2.5)
)
#> Prototype: <data.frame<x:double>>
#> 0. ( , <data.frame<x:logical>> ) = <data.frame<x:logical>>
#> 1. ( <data.frame<x:logical>> , <data.frame<x:integer>> ) = <data.frame<x:integer>>
#> 2. ( <data.frame<x:integer>> , <data.frame<x:double>> ) = <data.frame<x:double>>
そして、1つしか出てこない列の結合。:
vec_ptype_show(data.frame(x = 1, y = 1), data.frame(y = 1, z = 1))
#> Prototype: <data.frame<
#> x: double
#> y: double
#> z: double
#> >>
#> 0. ┌ , <data.frame< ┐ = <data.frame<
#> │ x: double │ x: double
#> │ y: double │ y: double
#> └ >> ┘ >>
#> 1. ┌ <data.frame< , <data.frame< ┐ = <data.frame<
#> │ x: double y: double │ x: double
#> │ y: double z: double │ y: double
#> │ >> >> │ z: double
#> └ ┘ >>
右側に新しい列が追加されていることに注意してください。これは、因子レベルやタイムゾーンの処理方法と一致しています。
vec_ptype_common()
は,ベクトルの集合の共通型を求めるものです。しかし、一般的に欲しいのは、その共通型に強制されたベクトルのセットです。これが vec_cast_common()
の仕事です。
str(vec_cast_common(
FALSE,
1:5,
2.5
))
#> List of 3
#> $ : num 0
#> $ : num [1:5] 1 2 3 4 5
#> $ : num 2.5
str(vec_cast_common(
factor("x"),
factor("y")
))
#> List of 2
#> $ : Factor w/ 2 levels "x","y": 1
#> $ : Factor w/ 2 levels "x","y": 2
str(vec_cast_common(
data.frame(x = 1),
data.frame(y = 1:2)
))
#> List of 2
#> $ :'data.frame': 1 obs. of 2 variables:
#> ..$ x: num 1
#> ..$ y: int NA
#> $ :'data.frame': 2 obs. of 2 variables:
#> ..$ x: num [1:2] NA NA
#> ..$ y: int [1:2] 1 2
また、vec_cast()
を使って特定のプロトタイプにキャストすることもできます。
# Cast succeeds
vec_cast(c(1, 2), integer())
#> [1] 1 2
# Cast fails
vec_cast(c(1.5, 2.5), factor("a"))
#> Error: Can't convert <double> to <factor<4d52a>>.
一般的なキャストは可能だが(例:double -> integer)、特定の入力に対して情報が失われる場合(例:1.5 -> 1)、エラーが発生します。
vec_cast(c(1.5, 2), integer())
#> Error: Can't convert from <double> to <integer> due to loss of precision.
#> * Locations: 1
allow_lossy_cast()
で、情報が失われるキャストのエラーを抑制することができます。
これにより、すべての情報が失われるキャストのエラーが抑制されます。許容される情報が失われるキャストのタイプを特定したい場合は、prototype を指定します。
allow_lossy_cast(
vec_cast(c(1.5, 2), integer()),
x_ptype = double(),
to_ptype = integer()
)
#> [1] 1 2
キャストのセットは、強制セットよりも寛容であってはなりません。これは強制されるものではありませんが、クラスにはこのルールに従い、強制エコシステムを健全に保つことが求められます。
vec_size()
は,データ構造中の 「オブザベーション」 の数を記述する不変量を持つ必要性から生まれたものです。 これはデータフレームでは特に重要で、f(data.frame(x))
とf(x)
が等しくなるような関数があると便利なのです。この特性を持つベース関数はありません。
length(data.frame(x))
は 1
に等しい。なぜなら、データフレームの長さは列数だからです。 これはデータフレームの長さが列の数だからである。
nrow(data.frame(x))
は nrow(x)
にはならない。 ベクトルのnrow()はNULL
だからである。
NROW(data.frame(x))
は、ベクトル x
に対して NROW(x)
と等しいので、ほぼ希望通りになります。 となります。しかし、NROW()
はlength()
の観点から定義されているので、すべてのオブジェクト、たとえ型が違っても、NROW(x)
を返します。 データフレームに入れられない型も含めて、すべてのオブジェクトの値を返します。 例えば、NROW(mean)
が 1
であっても data.frame(mean)
はエラーになります。
vec_size()
を以下のように定義します。
vec_size()
が与えられれば,データフレームの正確な定義を与えることができます:データフレームは,すべてのベクトルが同じサイズを持つベクトルのリストです。これは,行列とデータフレームの列を自明にサポートするという望ましい特性を持っています。
vec_slice()
は vec_size()
に対する [
に対する length()
のようなものです; つまり,基礎となるオブジェクトの次元に関わらず,オブザベーションを選択することができます.vec_slice(x, i)
は次のものと同じです。
x[i]
ただし x
がベクトルの時。x[i, , drop = FALSE]
ただし x
がデータフレームの時。x[i, , , drop = FALSE]
ただし x
が3次元配列の時。x <- sample(1:10)
df <- data.frame(x = x)
vec_slice(x, 5:6)
#> [1] 6 5
vec_slice(df, 5:6)
#> x
#> 1 6
#> 2 5
vec_slice(data.frame(x), i)
はdata.frame(vec_slice(x, i))
に等しい(変数名と行名はモジュロ)。
プロトタイプは vec_slice(x, 0L)
で生成されます。プロトタイプが与えられれば、vec_init()
で、与えられたサイズのベクトル(NA
で満たされる)を初期化することができます。
サイズの定義と密接な関係にあるのが、__リサイクルルール__です。リサイクルルールは、異なるサイズの2つのベクトルを結合したときの出力のサイズを決定します。vctrs では、リサイクルルールは vec_size_common()
でエンコードされており、これはベクトルの集合の共通サイズを与えます。
vec_size_common(1:3, 1:3, 1:3)
#> [1] 3
vec_size_common(1:10, 1)
#> [1] 10
vec_size_common(integer(), 1)
#> [1] 0
vctrs はbase R よりも厳しいリサイクルルールに従います。サイズ1のベクトルは他のサイズにリサイクルされます。他のサイズの組み合わせではエラーが発生します。この厳格さにより,dest == c("IAH", "HOU"))
のようなよくあるミスを防ぐことができますが,その代償として,rep()
の明示的な呼び出しが必要になることがあります。
リサイクルルールを適用するには2つの方法があります。
ベクトルと必要なサイズがあれば,vec_recycle()
を使います.
複数のベクトルがあり、それらを同じサイズにリサイクルしたい場合は、vec_recycle_common()
を使用してください。:
Base R のリサイクルルールはThe R Language Definitionに記載されていますが、単一の関数で実装されていないため、一貫して適用されていません。ここでは、その最も一般的な実現方法を簡単に説明し、いくつかの例外も示します。
一般的に Base Rでは、ベクトルのペアが同じ長さでない場合、短い方のベクトルは長い方と同じ長さにリサイクルされます。
rep(1, 6) + 1
#> [1] 2 2 2 2 2 2
rep(1, 6) + 1:2
#> [1] 2 3 2 3 2 3
rep(1, 6) + 1:3
#> [1] 2 3 4 2 3 4
長い方のベクトルの長さが、短い方のベクトルの長さの整数倍でない場合、通常は警告が表示されます。:
invisible(pmax(1:2, 1:3))
#> Warning in pmax(1:2, 1:3): 引数は部分的に再利用されます
invisible(1:2 + 1:3)
#> Warning in 1:2 + 1:3: 長いオブジェクトの長さが短いオブジェクトの長さの倍数になっ
#> ていません
invisible(cbind(1:2, 1:3))
#> Warning in cbind(1:2, 1:3): number of rows of result is not a multiple of vector
#> length (arg 1)
しかし、機能によっては静かにリサイクルするものもあります。:
length(atan2(1:3, 1:2))
#> [1] 3
length(paste(1:3, 1:2))
#> [1] 3
length(ifelse(1:3, 1:2, 1:2))
#> [1] 3
また、data.frame()
はエラーになります。:
data.frame(1:2, 1:3)
#> Error in data.frame(1:2, 1:3): arguments imply differing number of rows: 2, 3
R言語の定義では、「ゼロ長のベクトルを含む算術演算は、ゼロ長の結果を持つ」とされています。しかし、算術演算以外では、このルールは一貫して守られていません。
# length-0 output
1:2 + integer()
#> integer(0)
atan2(1:2, integer())
#> numeric(0)
pmax(1:2, integer())
#> integer(0)
# dropped
cbind(1:2, integer())
#> [,1]
#> [1,] 1
#> [2,] 2
# recycled to length of first
ifelse(rep(TRUE, 4), integer(), character())
#> [1] NA NA NA NA
# preserved-ish
paste(1:2, integer())
#> [1] "1 " "2 "
# Errors
data.frame(1:2, integer())
#> Error in data.frame(1:2, integer()): arguments imply differing number of rows: 2, 0