diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt index 1ed55b2143..ef7be10a37 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt @@ -148,7 +148,13 @@ public fun DataColumn.valueCounts( } if (dropNA) grouped = grouped.filter { !it.first.isNA } val nulls = if (dropNA) false else hasNulls() - val values = DataColumn.createByType(name(), grouped.map { it.first }, type().withNullability(nulls)) + val values = + if (grouped.isEmpty() && !isValueColumn()) { + // #2034 Preserve nested columns even when the input is empty + this.take(0) + } else { + DataColumn.createByType(name(), grouped.map { it.first }, type().withNullability(nulls)) + } val countName = if (resultColumn == name()) resultColumn + "1" else resultColumn val counts = DataColumn.createByType(countName, grouped.map { it.second }, typeOf()) return dataFrameOf(values, counts).cast() diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt index b0a3fcbfe3..e67a277251 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt @@ -1,6 +1,7 @@ package org.jetbrains.kotlinx.dataframe.api import io.kotest.matchers.shouldBe +import org.jetbrains.kotlinx.dataframe.DataColumn import org.jetbrains.kotlinx.dataframe.nrow import org.junit.Test import kotlin.reflect.typeOf @@ -104,6 +105,30 @@ class ValueCountsTests { result.rowsCount() shouldBe 0 } + @Test + fun `value counts of an empty nullable value column preserves type when NA values are included`() { + val input = DataColumn.createByType("city", emptyList(), typeOf()) + val result = input.valueCounts(dropNA = false) + + result.columnNames() shouldBe listOf("city", "count") + result.nrow shouldBe 0 + result["city"].isValueColumn() shouldBe true + result["city"].type() shouldBe typeOf() + result["count"].type() shouldBe typeOf() + } + + @Test + fun `value counts of an empty nullable value column drops nullability when NA values are excluded`() { + val input = DataColumn.createByType("city", emptyList(), typeOf()) + val result = input.valueCounts() + + result.columnNames() shouldBe listOf("city", "count") + result.nrow shouldBe 0 + result["city"].isValueColumn() shouldBe true + result["city"].type() shouldBe typeOf() + result["count"].type() shouldBe typeOf() + } + @Test fun `value counts of a column with a result column name clashing with the column name`() { val count by columnOf("a", "b", "a") @@ -265,5 +290,50 @@ class ValueCountsTests { df.drop(df.nrow).valueCounts().count() shouldBe 0 } + @Test + fun `value counts of an empty DataFrame retains all columns`() { + val result = df.take(0).valueCounts() + + result.columnNames() shouldBe listOf("name", "city", "age", "count") + result.nrow shouldBe 0 + result["name"].type() shouldBe typeOf() + result["age"].type() shouldBe typeOf() + result["count"].type() shouldBe typeOf() + } + + @Test + fun `value counts of an empty DataFrame retains selected columns`() { + val result = df.take(0).valueCounts("city") + + result.columnNames() shouldBe listOf("city", "count") + result.nrow shouldBe 0 + result["city"].type() shouldBe typeOf() + result["count"].type() shouldBe typeOf() + } + + @Test + fun `value counts retains selected columns when every row is dropped`() { + val input = dataFrameOf("city" to columnOf("London", null)).drop(1) + val result = input.valueCounts("city") + + result.columnNames() shouldBe listOf("city", "count") + result.nrow shouldBe 0 + result["city"].type() shouldBe typeOf() + result["count"].type() shouldBe typeOf() + } + + @Test + fun `value counts of an empty column group retains its observed columns`() { + val input = df.group("name", "city").into("details").take(0) + val result = input["details"].valueCounts() + + result.columnNames() shouldBe listOf("details", "count") + result["details"].asColumnGroup().columnNames() shouldBe listOf("name", "city") + result.nrow shouldBe 0 + result["details"].asColumnGroup()["name"].type() shouldBe typeOf() + result["details"].asColumnGroup()["city"].type() shouldBe typeOf() + result["count"].type() shouldBe typeOf() + } + // endregion }