Равномерность DataFrame в Apache Spark
Предположим, что df1
и df2
являются двумя DataFrame
в Apache Spark, вычисленными с использованием двух разных механизмов, например Spark SQL и API Scala/Java/Python.
Существует ли идиоматический способ определить, являются ли два кадра данных эквивалентными (равными, изоморфными), где эквивалентность определяется данными (имена столбцов и значения столбцов для каждой строки) идентичны, кроме упорядочения строк и столбцов
Мотивация вопроса заключается в том, что часто существует множество способов вычисления большого результата данных, каждый из которых имеет свои собственные компромиссы. По мере изучения этих компромиссов важно поддерживать правильность и, следовательно, необходимо проверить эквивалентность/равенство в значимом наборе тестовых данных.
Ответы
Ответ 1
В наборах Apache Spark есть несколько стандартных способов, однако большинство из них предполагает сбор данных локально, и если вы хотите провести тестирование равенства на больших DataFrames, то это, вероятно, не подходит.
Сначала проверьте схему, а затем вы можете сделать перекресток с df3 и убедиться, что количество df1, df2 и df3 равно (хотя это работает только в том случае, если нет повторяющихся строк, если есть разные строки дубликатов, это метод все равно может вернуть true).
Другим вариантом будет получение базовых RDD обоих DataFrames, сопоставление с (Row, 1), выполнение reduceByKey для подсчета числа каждого Row и последующее объединение двух полученных RDD, а затем выполнение регулярного агрегата и return false, если какой-либо из итераторов не равен.
Ответ 2
Я не знаю об идиоматике, но я думаю, вы можете получить надежный способ сравнить DataFrames, как вы описываете следующим образом. (Я использую 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
Этот подход правильно обрабатывает случаи, когда в DataFrames могут быть дублированные строки, строки в разных порядках и/или столбцы в разных порядках.
Например:
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()
, что, вероятно, ускорит процесс.
Ответ 3
Библиотека spark-fast-tests имеет два метода для сравнения DataFrame (я являюсь создателем библиотеки):
Метод assertSmallDataFrameEquality
собирает DataFrames в драйвере node и делает сравнение
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
сравнивает распространение DataFrames на нескольких машинах (код в основном копируется из 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, и я нашел его достаточным для своих тестовых наборов.
Ответ 4
Java:
assert resultDs.union(answerDs).distinct().count() == resultDs.intersect(answerDs).count();
Ответ 5
Вы можете сделать это, используя небольшую дедупликацию в сочетании с полным внешним соединением. Преимущество такого подхода заключается в том, что он не требует, чтобы вы собирали результаты для драйвера и избегали запуска нескольких заданий.
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()
Ответ 6
Попробуйте сделать следующее:
df1.except(df2).isEmpty
Ответ 7
try {
return ds1.union(ds2)
.groupBy(columns(ds1, ds1.columns()))
.count()
.filter("count % 2 > 0")
.count()
== 0;
} catch (Exception e) {
return false;
}
Column[] columns(Dataset<Row> ds, String... columnNames) {
List<Column> l = new ArrayList<>();
for (String cn : columnNames) {
l.add(ds.col(cn));
}
return l.stream().toArray(Column[]::new);}
метод столбцов является дополнительным и может быть заменен любым методом, который возвращает Seq
Логика:
- Объединение обоих наборов данных, если столбцы не совпадают, будет выдано исключение и, следовательно, вернет false.
- Если столбцы совпадают, то groupBy для всех столбцов и добавить количество столбцов. Теперь все строки имеют количество, кратное 2 (даже для повторяющихся строк).
- Проверьте, есть ли какая-либо строка с числом, не делимым на 2, это дополнительные строки.