df1
およびdf2
は、Apache Sparkの2つのDataFrame
です。2つの異なるメカニズムを使用して計算されます。たとえば、Spark SQLとScala/Java/Python API。
行と列の順序を保存せずにデータ(列名と各行の列値)が同じであると同等である場合、2つのデータフレームが等しい(等しい、同形)かどうかを判定する慣用的な方法はありますか?
質問の動機は、多くの場合、ビッグデータの結果を計算する多くの方法があり、それぞれにトレードオフがあることです。これらのトレードオフを検討する際、正確性を維持することが重要であるため、意味のあるテストデータセットの等価性/等価性をチェックする必要があります。
Apache Sparkテストスイートにはいくつかの標準的な方法がありますが、これらのほとんどはローカルでデータを収集する必要があり、大規模なDataFrameで同等性テストを行いたい場合、適切なソリューションではない可能性があります。
最初にスキーマをチェックしてからdf3との交差を行い、df1、df2およびdf3のカウントがすべて等しいことを確認できます(ただし、これは重複行がない場合にのみ機能しますが、異なる重複行がある場合はこの方法でも可能です) trueを返します。
別のオプションは、両方のDataFramesの基礎となるRDDを取得し、(Row、1)にマッピングし、reduceByKeyを実行して各行の数をカウントし、結果の2つのRDDをグループ化し、通常の集約を行い、次の場合にfalseを返しますいずれのイテレーターも等しくありません。
イディオムについては知りませんが、次のように説明するように、DataFrameを比較する堅牢な方法を得ることができると思います。 (説明のためにPySparkを使用していますが、このアプローチは複数の言語に適用されます。)
_a = spark.range(5)
b = spark.range(5)
a_prime = a.groupBy(sorted(a.columns)).count()
b_prime = b.groupBy(sorted(b.columns)).count()
assert a_prime.subtract(b_prime).count() == b_prime.subtract(a_prime).count() == 0
_
このアプローチは、データフレームに重複行、異なる順序の行、および/または異なる順序の列がある場合を正しく処理します。
例えば:
_a = spark.createDataFrame([('nick', 30), ('bob', 40)], ['name', 'age'])
b = spark.createDataFrame([(40, 'bob'), (30, 'nick')], ['age', 'name'])
c = spark.createDataFrame([('nick', 30), ('bob', 40), ('nick', 30)], ['name', 'age'])
a_prime = a.groupBy(sorted(a.columns)).count()
b_prime = b.groupBy(sorted(b.columns)).count()
c_prime = c.groupBy(sorted(c.columns)).count()
assert a_prime.subtract(b_prime).count() == b_prime.subtract(a_prime).count() == 0
assert a_prime.subtract(c_prime).count() != 0
_
このアプローチは非常に高価ですが、完全な差分を実行する必要があるため、ほとんどの費用は避けられません。また、ローカルで何かを収集する必要がないので、これはうまくスケールするはずです。比較で重複行を考慮すべき制約を緩和する場合は、groupBy()
を削除してsubtract()
を実行するだけで、おそらく速度が大幅に向上します。
spark-fast-tests ライブラリには、DataFrameの比較を行うための2つのメソッドがあります(私はライブラリの作成者です)。
assertSmallDataFrameEquality
メソッドは、ドライバーノードでDataFramesを収集し、比較を行います
def assertSmallDataFrameEquality(actualDF: DataFrame, expectedDF: DataFrame): Unit = {
if (!actualDF.schema.equals(expectedDF.schema)) {
throw new DataFrameSchemaMismatch(schemaMismatchMessage(actualDF, expectedDF))
}
if (!actualDF.collect().sameElements(expectedDF.collect())) {
throw new DataFrameContentMismatch(contentMismatchMessage(actualDF, expectedDF))
}
}
assertLargeDataFrameEquality
メソッドは、複数のマシンに分散したDataFrameを比較します(コードは基本的に spark-testing-base からコピーされます)
def assertLargeDataFrameEquality(actualDF: DataFrame, expectedDF: DataFrame): Unit = {
if (!actualDF.schema.equals(expectedDF.schema)) {
throw new DataFrameSchemaMismatch(schemaMismatchMessage(actualDF, expectedDF))
}
try {
actualDF.rdd.cache
expectedDF.rdd.cache
val actualCount = actualDF.rdd.count
val expectedCount = expectedDF.rdd.count
if (actualCount != expectedCount) {
throw new DataFrameContentMismatch(countMismatchMessage(actualCount, expectedCount))
}
val expectedIndexValue = zipWithIndex(actualDF.rdd)
val resultIndexValue = zipWithIndex(expectedDF.rdd)
val unequalRDD = expectedIndexValue
.join(resultIndexValue)
.filter {
case (idx, (r1, r2)) =>
!(r1.equals(r2) || RowComparer.areRowsEqual(r1, r2, 0.0))
}
val maxUnequalRowsToShow = 10
assertEmpty(unequalRDD.take(maxUnequalRowsToShow))
} finally {
actualDF.rdd.unpersist()
expectedDF.rdd.unpersist()
}
}
assertSmallDataFrameEquality
は小規模なDataFrame比較で高速であり、テストスイートで十分であることがわかりました。
Java:
assert resultDs.union(answerDs).distinct().count() == resultDs.intersect(answerDs).count();
これを行うには、完全な外部結合と組み合わせて少しの重複排除を使用します。このアプローチの利点は、ドライバーに結果を収集する必要がなく、複数のジョブの実行を回避できることです。
import org.Apache.spark.sql._
import org.Apache.spark.sql.functions._
// Generate some random data.
def random(n: Int, s: Long) = {
spark.range(n).select(
(Rand(s) * 10000).cast("int").as("a"),
(Rand(s + 5) * 1000).cast("int").as("b"))
}
val df1 = random(10000000, 34)
val df2 = random(10000000, 17)
// Move all the keys into a struct (to make handling nulls easy), deduplicate the given dataset
// and count the rows per key.
def dedup(df: Dataset[Row]): Dataset[Row] = {
df.select(struct(df.columns.map(col): _*).as("key"))
.groupBy($"key")
.agg(count(lit(1)).as("row_count"))
}
// Deduplicate the inputs and join them using a full outer join. The result can contain
// the following things:
// 1. Both keys are not null (and thus equal), and the row counts are the same. The dataset
// is the same for the given key.
// 2. Both keys are not null (and thus equal), and the row counts are not the same. The dataset
// contains the same keys.
// 3. Only the right key is not null.
// 4. Only the left key is not null.
val joined = dedup(df1).as("l").join(dedup(df2).as("r"), $"l.key" === $"r.key", "full")
// Summarize the differences.
val summary = joined.select(
count(when($"l.key".isNotNull && $"r.key".isNotNull && $"r.row_count" === $"l.row_count", 1)).as("left_right_same_rc"),
count(when($"l.key".isNotNull && $"r.key".isNotNull && $"r.row_count" =!= $"l.row_count", 1)).as("left_right_different_rc"),
count(when($"l.key".isNotNull && $"r.key".isNull, 1)).as("left_only"),
count(when($"l.key".isNull && $"r.key".isNotNull, 1)).as("right_only"))
summary.show()
次のことを試してください。
df1.except(df2).isEmpty