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 5a49c050a0..d7bf8ad691 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 @@ -10,16 +10,77 @@ import org.jetbrains.kotlinx.dataframe.annotations.Interpretable import org.jetbrains.kotlinx.dataframe.annotations.Refine import org.jetbrains.kotlinx.dataframe.annotations.RequiredByIntellijPlugin import org.jetbrains.kotlinx.dataframe.columns.toColumnSet +import org.jetbrains.kotlinx.dataframe.documentation.DocumentationUrls +import org.jetbrains.kotlinx.dataframe.documentation.ExcludeFromSources +import org.jetbrains.kotlinx.dataframe.documentation.NALink +import org.jetbrains.kotlinx.dataframe.documentation.SelectingColumns import org.jetbrains.kotlinx.dataframe.impl.nameGenerator import org.jetbrains.kotlinx.dataframe.util.DEPRECATED_ACCESS_API import kotlin.reflect.KProperty import kotlin.reflect.full.withNullability import kotlin.reflect.typeOf +// region docs + +/** + * @param [sort\] If `true` (default), the result is sorted by count. + * Otherwise, the counted values keep the order of their first occurrence. + * @param [ascending\] The sorting direction. If `false` (default), the most frequent values come first. + * Only used when [sort\] is `true`. + * @param [dropNA\] If `true` (default), {@include [NALink]} values (`null`s and `NaN`s) are not counted + * and are excluded from the result. + * @param [resultColumn\] The name of the resulting count column. Default — `"count"`. + * If a column with this name is already present, the name is made unique by appending + * a number to it (for example, `"count"` becomes `"count1"`). + */ +@ExcludeFromSources +private typealias ValueCountsParams = Nothing + +/** + * Returns a [DataFrame] containing the counts of unique rows (or combinations of selected values) in this [DataFrame]. + * + * Rows are compared by the values in the selected [columns\]. If no columns are selected, values from + * all columns are used. + * + * {@include [SelectingColumns.ColumnGroupsAndNestedColumnsSnippet]} + * + * The resulting [DataFrame] contains: + * - the distinct combinations of these values, + * - a new [Int] column (named [resultColumn\], default is `"count"`) with the number of occurrences of each combination. + * + * By default, the result is sorted by count in descending order (the most frequent combination first), + * and a row is not counted at all if any of its selected values is {@include [NALink]}. + * + * See also: + * - [`valueCounts`][DataColumn.valueCounts] — counts of unique values in a single [DataColumn]. + * - [`countDistinct`][DataFrame.countDistinct] — the number of distinct rows or selected value combinations, + * without their counts. + * - [`distinct`][DataFrame.distinct] — the distinct rows themselves, without their counts. + * - [`count`][DataFrame.count] — the total number of rows in this [DataFrame]. + * + * For more information: {@include [DocumentationUrls.ValueCounts]} + * + * All summary statistics: {@include [DocumentationUrls.Statistics]} + * + * ### This `valueCounts` Overload + */ +@ExcludeFromSources +private typealias CommonValueCountsDocs = Nothing + +// endregion + // region DataSchema +/** + * A [DataSchema] of the [valueCounts] result. It declares the [count] column + * with the number of occurrences of each counted value. + * + * For more information: {@include [DocumentationUrls.ValueCounts]} + */ @DataSchema public interface ValueCount { + + /** The number of occurrences of the value (or combination of values) in this row. */ public val count: Int } @@ -29,6 +90,42 @@ public interface ValueCount { internal val defaultCountColumnName: String = ValueCount::count.name +/** + * Returns a [DataFrame] containing counts of unique values in this [DataColumn]. + * + * The resulting [DataFrame] contains: + * - the column with the distinct values of the original [DataColumn] + * - a new [Int] column ([resultColumn]) with the number of occurrences of each value. + * + * By default, the result is sorted by count in descending order (the most frequent value first), + * and {@include [NALink]} values (`null`s and `NaN`s) are not counted. + * + * See also: + * - [`valueCounts`][DataFrame.valueCounts] — counts of unique rows in a [DataFrame]. + * - [`countDistinct`][DataFrame.countDistinct] — the number of distinct rows + * or selected value combinations in a [DataFrame]. + * - [`distinct`][DataColumn.distinct] — the distinct values themselves, without their counts. + * - [`count`][DataColumn.count] — the total number of elements in this [DataColumn]. + * + * For more information: {@include [DocumentationUrls.ValueCounts]} + * + * All summary statistics: {@include [DocumentationUrls.Statistics]} + * + * ### Example + * + * ```kotlin + * // Counts the unique values in the "city" column, + * // starting with the most frequent one + * df.city.valueCounts() + * + * // Counts the unique values in the "city" column, including `null`s, + * // in the order of their first occurrence, in a column named "quantity" + * df.city.valueCounts(sort = false, dropNA = false, resultColumn = "quantity") + * ``` + * + * @include [ValueCountsParams] + * @return A [DataFrame] with the distinct values of this [DataColumn] and their counts. + */ @RequiredByIntellijPlugin public fun DataColumn.valueCounts( sort: Boolean = true, @@ -56,6 +153,27 @@ public fun DataColumn.valueCounts( // region DataFrame +/** + * {@include [CommonValueCountsDocs]} + * {@include [SelectingColumns.ColumnsSelectionDsl]} + * + * #### Example + * + * ```kotlin + * // Counts the unique combinations of the values in the "name" and "city" columns, + * // starting with the most frequent one + * df.valueCounts { name and city } + * + * // Counts the unique rows of the whole dataframe, + * // including rows with `NA` values, in the order of their first occurrence + * df.valueCounts(sort = false, dropNA = false) + * ``` + * + * @include [ValueCountsParams] + * @param [columns] The optional [ColumnsSelector] that selects the columns whose distinct value combinations + * are counted. If `null` or omitted, all columns are used. + * @return A [DataFrame] with the distinct value combinations and their counts. + */ @Refine @Interpretable("ValueCounts") public fun DataFrame.valueCounts( @@ -78,6 +196,23 @@ public fun DataFrame.valueCounts( .cast() } +/** + * {@include [CommonValueCountsDocs]} + * {@include [SelectingColumns.ColumnNamesApi]} + * + * #### Example + * + * ```kotlin + * // Counts the unique combinations of the values in the "name" and "city" columns, + * // starting with the most frequent one + * df.valueCounts("name", "city") + * ``` + * + * @param [columns] (optional) The names of the columns whose distinct value combinations are counted. If not supplied, + * all columns are selected. + * @include [ValueCountsParams] + * @return A [DataFrame] with the distinct value combinations and their counts. + */ public fun DataFrame.valueCounts( vararg columns: String, sort: Boolean = true, diff --git a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt index 949e7dc527..c5b5be4077 100644 --- a/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt +++ b/core/src/main/kotlin/org/jetbrains/kotlinx/dataframe/documentation/DocumentationUrls.kt @@ -140,6 +140,9 @@ public interface DocumentationUrls { /** [See `countDistinct` on the documentation website.]({@include [Url]}/countdistinct.html) */ public typealias CountDistinct = Nothing + /** [See `valueCounts` on the documentation website.]({@include [Url]}/valuecounts.html) */ + public typealias ValueCounts = Nothing + /** [See `explode` on the documentation website.]({@include [Url]}/explode.html) */ public typealias Explode = Nothing 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 new file mode 100644 index 0000000000..b0a3fcbfe3 --- /dev/null +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/api/valueCounts.kt @@ -0,0 +1,269 @@ +package org.jetbrains.kotlinx.dataframe.api + +import io.kotest.matchers.shouldBe +import org.jetbrains.kotlinx.dataframe.nrow +import org.junit.Test +import kotlin.reflect.typeOf + +class ValueCountsTests { + + private val df = dataFrameOf( + "name" to columnOf("Alice", "Bob", "Alice", "Charlie", "Alice"), + "city" to columnOf("London", "London", "London", "Moscow", "Paris"), + "age" to columnOf(15, 20, 15, 30, 15), + ) + + // region DataColumn + + @Test + fun `value counts of a column`() { + val languages by columnOf("Kotlin", "Kotlin", null, null, "C++") + val languageCounts = languages.valueCounts() + languageCounts["languages"].values() shouldBe listOf("Kotlin", "C++") + languageCounts.count.values() shouldBe listOf(2, 1) + } + + @Test + fun `value counts of a column keeps the column name and adds an Int count column`() { + val result = df["city"].valueCounts() + + result.columnNames() shouldBe listOf("city", "count") + result["city"].type() shouldBe typeOf() + result["count"].type() shouldBe typeOf() + } + + @Test + fun `value counts of a column is sorted by count descending by default`() { + val result = df["city"].valueCounts() + + result["city"].values() shouldBe listOf("London", "Moscow", "Paris") + result["count"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `value counts of a column with ascending sorting`() { + val result = df["city"].valueCounts(ascending = true) + + result["city"].values() shouldBe listOf("Moscow", "Paris", "London") + result["count"].values() shouldBe listOf(1, 1, 3) + } + + @Test + fun `value counts of a column without sorting keeps the order of the first occurrence`() { + val result = df["name"].valueCounts(sort = false) + + result["name"].values() shouldBe listOf("Alice", "Bob", "Charlie") + result["count"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `value counts of a column drops NA values by default`() { + val values by columnOf(1.0, null, Double.NaN, 1.0, null, 2.0) + val result = values.valueCounts() + + result["values"].values() shouldBe listOf(1.0, 2.0) + result["values"].type() shouldBe typeOf() + result.count.values() shouldBe listOf(2, 1) + } + + @Test + fun `value counts of a column with dropNA false counts nulls`() { + val values by columnOf(1.0, null, 1.0, 2.0, null) + val result = values.valueCounts(dropNA = false, sort = false) + + result["values"].values() shouldBe listOf(1.0, null, 2.0) + result["values"].type() shouldBe typeOf() + result.count.values() shouldBe listOf(2, 2, 1) + } + + @Test + fun `value counts of a column with dropNA false counts NaNs`() { + val values by columnOf(1.0, Double.NaN, 1.0, Double.NaN, 2.0) + val result = values.valueCounts(dropNA = false, sort = false) + val resultValues = result["values"].values().toList() + + resultValues[0] shouldBe 1.0 + (resultValues[1] as Double).isNaN() shouldBe true + resultValues[2] shouldBe 2.0 + result.count.values() shouldBe listOf(2, 2, 1) + } + + @Test + fun `value counts of a column with a custom result column name`() { + val result = df["city"].valueCounts(resultColumn = "number") + + result.columnNames() shouldBe listOf("city", "number") + result["number"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `value counts of an empty column`() { + val result = df["city"].filter { false }.valueCounts() + + result.columnNames() shouldBe listOf("city", "count") + result.rowsCount() shouldBe 0 + } + + @Test + fun `value counts of a column with a result column name clashing with the column name`() { + val count by columnOf("a", "b", "a") + val result = count.valueCounts() + + result.columnNames() shouldBe listOf("count", "count1") + result["count"].values() shouldBe listOf("a", "b") + result["count1"].values() shouldBe listOf(2, 1) + } + + // endregion + + // region DataFrame + + @Test + fun `value counts of a DataFrame counts distinct rows`() { + val result = df.valueCounts() + + result shouldBe dataFrameOf( + "name" to columnOf("Alice", "Bob", "Charlie", "Alice"), + "city" to columnOf("London", "London", "Moscow", "Paris"), + "age" to columnOf(15, 20, 30, 15), + "count" to columnOf(2, 1, 1, 1), + ) + } + + @Test + fun `value counts of a DataFrame with a columns selector`() { + val result = df.valueCounts { "name"() and "city"() } + + result shouldBe dataFrameOf( + "name" to columnOf("Alice", "Bob", "Charlie", "Alice"), + "city" to columnOf("London", "London", "Moscow", "Paris"), + "count" to columnOf(2, 1, 1, 1), + ) + } + + @Test + fun `value counts of a DataFrame with a single selected column`() { + val result = df.valueCounts { "city"() } + + result shouldBe dataFrameOf( + "city" to columnOf("London", "Moscow", "Paris"), + "count" to columnOf(3, 1, 1), + ) + } + + @Test + fun `value counts of a DataFrame by column names`() { + df.valueCounts("name", "city") shouldBe df.valueCounts { "name"() and "city"() } + } + + @Test + fun `value counts of a DataFrame without sorting keeps the order of the first occurrence`() { + val result = df.valueCounts(sort = false) { "city"() } + + result["city"].values() shouldBe listOf("London", "Moscow", "Paris") + result["count"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `value counts of a DataFrame with ascending sorting`() { + val result = df.valueCounts(ascending = true) { "city"() } + + result["city"].values() shouldBe listOf("Moscow", "Paris", "London") + result["count"].values() shouldBe listOf(1, 1, 3) + } + + @Test + fun `value counts of a DataFrame drops rows with NA in any selected column by default`() { + val dfWithNulls = dataFrameOf( + "name" to columnOf("Alice", "Bob", null, "Alice"), + "city" to columnOf("London", null, "Moscow", "London"), + "weight" to columnOf(50.0, 60.0, null, Double.NaN), + ) + val result = dfWithNulls.valueCounts() + + result.columnNames() shouldBe listOf("name", "city", "weight", "count") + result["name"].values() shouldBe listOf("Alice") + result["city"].values() shouldBe listOf("London") + result["count"].values() shouldBe listOf(1) + } + + @Test + fun `value counts of a DataFrame ignores NA in columns that are not selected`() { + val dfWithNulls = dataFrameOf( + "name" to columnOf("Alice", "Bob", "Alice"), + "city" to columnOf("London", null, "London"), + "weight" to columnOf(50.0, 60.0, Double.NaN), + ) + val result = dfWithNulls.valueCounts { "name"() } + + result.columnNames() shouldBe listOf("name", "count") + result["name"].values() shouldBe listOf("Alice", "Bob") + result["count"].values() shouldBe listOf(2, 1) + } + + @Test + fun `value counts of a DataFrame with dropNA false counts rows with NA`() { + val dfWithNulls = dataFrameOf( + "name" to columnOf("Alice", "Bob", null, "Alice"), + "city" to columnOf("London", null, "Moscow", "London"), + "weight" to columnOf(50.0, 60.0, null, Double.NaN), + ) + val result = dfWithNulls.valueCounts(dropNA = false, sort = false) + val weightValues = result["weight"].values().toList() + + result["name"].values() shouldBe listOf("Alice", "Bob", null, "Alice") + result["city"].values() shouldBe listOf("London", null, "Moscow", "London") + result["count"].values() shouldBe listOf(1, 1, 1, 1) + weightValues[0] shouldBe 50.0 + weightValues[1] shouldBe 60.0 + weightValues[2] shouldBe null + (weightValues[3] as Double).isNaN() shouldBe true + } + + @Test + fun `value counts of a DataFrame with a custom result column name`() { + val result = df.valueCounts(resultColumn = "occurrences") { "city"() } + + result.columnNames() shouldBe listOf("city", "occurrences") + result["city"].values() shouldBe listOf("London", "Moscow", "Paris") + result["occurrences"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `value counts of a DataFrame with a result column name clashing with a selected column`() { + val result = df.valueCounts(resultColumn = "city") { "city"() } + + result.columnNames() shouldBe listOf("city", "city1") + result["city"].values() shouldBe listOf("London", "Moscow", "Paris") + result["city1"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `value counts of a DataFrame with a result column name clashing with a column that is not selected`() { + val result = df.valueCounts(resultColumn = "age") { "city"() } + + result.columnNames() shouldBe listOf("city", "age1") + result["age1"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `value counts of a DataFrame with a default result column name clashing with an existing column`() { + val dfWithCount = df.add("count") { 1 } + val result = dfWithCount.valueCounts { "city"() } + + result.columnNames() shouldBe listOf("city", "count1") + result["count1"].values() shouldBe listOf(3, 1, 1) + } + + @Test + fun `count column of a DataFrame result is of type Int`() { + df.valueCounts()["count"].type() shouldBe typeOf() + } + + @Test + fun `value counts of an empty DataFrame`() { + df.drop(df.nrow).valueCounts().count() shouldBe 0 + } + + // endregion +} diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Analyze.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Analyze.kt index 78eabd1161..2625bc6243 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Analyze.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/Analyze.kt @@ -39,7 +39,6 @@ import org.jetbrains.kotlinx.dataframe.api.stdOf import org.jetbrains.kotlinx.dataframe.api.sum import org.jetbrains.kotlinx.dataframe.api.sumFor import org.jetbrains.kotlinx.dataframe.api.sumOf -import org.jetbrains.kotlinx.dataframe.api.valueCounts import org.jetbrains.kotlinx.dataframe.explainer.TransformDataFrameExpressions import org.junit.Test import kotlin.math.ln @@ -475,14 +474,4 @@ class Analyze : TestBase() { df.groupBy { city }.cumSum { weight }.concat() // SampleEnd } - - @Test - @TransformDataFrameExpressions - fun valueCounts() { - // SampleStart - df.city.valueCounts() - - df.valueCounts { name and city } - // SampleEnd - } } diff --git a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/DataColumnTests.kt b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/DataColumnTests.kt index 9e97978e5d..83ae9780c1 100644 --- a/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/DataColumnTests.kt +++ b/core/src/test/kotlin/org/jetbrains/kotlinx/dataframe/testSets/person/DataColumnTests.kt @@ -1,13 +1,10 @@ package org.jetbrains.kotlinx.dataframe.testSets.person import io.kotest.matchers.shouldBe -import org.jetbrains.kotlinx.dataframe.api.columnOf -import org.jetbrains.kotlinx.dataframe.api.count import org.jetbrains.kotlinx.dataframe.api.sort import org.jetbrains.kotlinx.dataframe.api.sortBy import org.jetbrains.kotlinx.dataframe.api.sortByDesc import org.jetbrains.kotlinx.dataframe.api.sortDesc -import org.jetbrains.kotlinx.dataframe.api.valueCounts import org.junit.Test class DataColumnTests : BaseTest() { @@ -17,12 +14,4 @@ class DataColumnTests : BaseTest() { typed.age.sort() shouldBe typed.sortBy { age }.age typed.age.sortDesc() shouldBe typed.sortByDesc { age }.age } - - @Test - fun `value counts`() { - val languages by columnOf("Kotlin", "Kotlin", null, null, "C++") - val languageCounts = languages.valueCounts() - languageCounts[languages].values() shouldBe listOf("Kotlin", "C++") - languageCounts.count.values() shouldBe listOf(2, 1) - } } diff --git a/docs/StardustDocs/resources/api/pivot/pivotInward_properties.html b/docs/StardustDocs/resources/api/pivot/pivotInward_properties.html index 711be6bb1c..ab330b21f4 100644 --- a/docs/StardustDocs/resources/api/pivot/pivotInward_properties.html +++ b/docs/StardustDocs/resources/api/pivot/pivotInward_properties.html @@ -459,7 +459,7 @@ /**/ diff --git a/docs/StardustDocs/resources/api/valueCounts/valueCounts.html b/docs/StardustDocs/resources/api/valueCounts/valueCounts.html new file mode 100644 index 0000000000..83913641e1 --- /dev/null +++ b/docs/StardustDocs/resources/api/valueCounts/valueCounts.html @@ -0,0 +1,512 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/api/valueCounts/valueCountsColumn.html b/docs/StardustDocs/resources/api/valueCounts/valueCountsColumn.html new file mode 100644 index 0000000000..fbf0429038 --- /dev/null +++ b/docs/StardustDocs/resources/api/valueCounts/valueCountsColumn.html @@ -0,0 +1,511 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/api/valueCounts/valueCountsDf.html b/docs/StardustDocs/resources/api/valueCounts/valueCountsDf.html new file mode 100644 index 0000000000..770a76f736 --- /dev/null +++ b/docs/StardustDocs/resources/api/valueCounts/valueCountsDf.html @@ -0,0 +1,511 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/api/valueCounts/valueCountsSelector_properties.html b/docs/StardustDocs/resources/api/valueCounts/valueCountsSelector_properties.html new file mode 100644 index 0000000000..4ad14e034e --- /dev/null +++ b/docs/StardustDocs/resources/api/valueCounts/valueCountsSelector_properties.html @@ -0,0 +1,511 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/api/valueCounts/valueCountsWithNA.html b/docs/StardustDocs/resources/api/valueCounts/valueCountsWithNA.html new file mode 100644 index 0000000000..56040bc78a --- /dev/null +++ b/docs/StardustDocs/resources/api/valueCounts/valueCountsWithNA.html @@ -0,0 +1,512 @@ + + + + + +
+ +

+ + + diff --git a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.Analyze.valueCounts.html b/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.Analyze.valueCounts.html deleted file mode 100644 index e8f152906d..0000000000 --- a/docs/StardustDocs/resources/snippets/org.jetbrains.kotlinx.dataframe.samples.api.Analyze.valueCounts.html +++ /dev/null @@ -1,638 +0,0 @@ - - - - - -
- df.valueCounts() -
- Input DataFrame: rowsCount = 7, columnsCount = 5 -
- -

-
-
- Output DataFrame: rowsCount = 5, columnsCount = 2 -
- -

-
-
-
-
- df.valueCounts { name and city } -
- Input DataFrame: rowsCount = 7, columnsCount = 5 -
- -

-
-
- Output DataFrame: rowsCount = 6, columnsCount = 3 -
- -

-
-
-
- - - diff --git a/docs/StardustDocs/topics/_shadow_resources.md b/docs/StardustDocs/topics/_shadow_resources.md index 5177946e55..b92b8613c1 100644 --- a/docs/StardustDocs/topics/_shadow_resources.md +++ b/docs/StardustDocs/topics/_shadow_resources.md @@ -191,6 +191,11 @@ + + + + + @@ -261,7 +266,6 @@ - diff --git a/docs/StardustDocs/topics/valueCounts.md b/docs/StardustDocs/topics/valueCounts.md index aae0fc8b66..f2e4962073 100644 --- a/docs/StardustDocs/topics/valueCounts.md +++ b/docs/StardustDocs/topics/valueCounts.md @@ -1,30 +1,86 @@ [//]: # (title: valueCounts) - + -Return [`DataFrame`](DataFrame.md) containing counts of unique values in [`DataFrame`](DataFrame.md) +Returns a [`DataFrame`](DataFrame.md) containing the counts of the unique values in a [`DataFrame`](DataFrame.md) or [`DataColumn`](DataColumn.md). ```kotlin -valueCounts(sort = true, ascending = false, dropNA = false) +valueCounts(sort = true, ascending = false, dropNA = true, resultColumn = "count") [ { columns } ] ``` See [column selectors](ColumnSelectors.md) for how to select the columns for this operation. **Parameters:** -* `sort: Boolean = true` — sort by count -* `ascending: Boolean = false` — sort in ascending order -* `dropNA: Boolean = true` — don't include counts of [`NA`](nanAndNa.md) values -* `columns = all` — columns to use when counting unique combinations +* `sort: Boolean = true` — whether to sort by count +* `ascending: Boolean = false` — sort direction (by default most frequent first) +* `dropNA: Boolean = true` — whether to exclude [`NA`](nanAndNa.md) values from counting (excluded by default) +* `resultColumn: String = "count"` — name of the column with counts +* `columns = all` — columns to use when counting unique combinations + +The following dataframe will be used in the examples below: + + + +```kotlin +df +``` + + + + +For a [`DataFrame`](DataFrame.md), `dropNA = true` (default) excludes the entire row if any selected column contains +an [`NA`](nanAndNa.md) value. [`NA`](nanAndNa.md) values in columns that are not selected do not affect the result. ```kotlin -df.city.valueCounts() +df.valueCounts() +``` + + + + +To include rows with [`NA`](nanAndNa.md) values, set `dropNA = false`: + + + +```kotlin +df.valueCounts(dropNA = false) +``` + + + + +You can specify columns that will be used to determine uniqueness: + + + + + +```kotlin +df.valueCounts(dropNA = false) { name } +``` + + + + +```kotlin +df.valueCounts("name", dropNA = false) +``` + + + + + +This operation can also be applied to a [`DataColumn`](DataColumn.md): -df.valueCounts { name and city } + + +```kotlin +df.age.valueCounts() ``` - + diff --git a/samples/build.gradle.kts b/samples/build.gradle.kts index 7cf0ccfd52..08d513d05b 100644 --- a/samples/build.gradle.kts +++ b/samples/build.gradle.kts @@ -117,6 +117,7 @@ korro { include("groupBy.md") include("pivot.md") include("countDistinct.md") + include("valueCounts.md") }, ) baseDir = rootProject.file("docs/StardustDocs/topics") diff --git a/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/ValueCountsSamples.kt b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/ValueCountsSamples.kt new file mode 100644 index 0000000000..7a086696f4 --- /dev/null +++ b/samples/src/test/kotlin/org/jetbrains/kotlinx/dataframe/samples/api/ValueCountsSamples.kt @@ -0,0 +1,101 @@ +package org.jetbrains.kotlinx.dataframe.samples.api + +import org.jetbrains.kotlinx.dataframe.DataFrame +import org.jetbrains.kotlinx.dataframe.annotations.DataSchema +import org.jetbrains.kotlinx.dataframe.api.RgbColor +import org.jetbrains.kotlinx.dataframe.api.and +import org.jetbrains.kotlinx.dataframe.api.cast +import org.jetbrains.kotlinx.dataframe.api.dataFrameOf +import org.jetbrains.kotlinx.dataframe.api.format +import org.jetbrains.kotlinx.dataframe.api.perRowCol +import org.jetbrains.kotlinx.dataframe.api.valueCounts +import org.jetbrains.kotlinx.dataframe.samples.DataFrameSampleHelper +import org.jetbrains.kotlinx.dataframe.util.defaultHeaderFormatting +import org.junit.Test + +class ValueCountsSamples : DataFrameSampleHelper("valueCounts", "api") { + @DataSchema + interface SimplePerson { + val name: String + val age: Int? + } + + private val df: DataFrame = dataFrameOf( + "name" to listOf("Alice", "Bob", "Charlie", "Alice", "Alice"), + "age" to listOf(15, 20, 25, 15, null), + ).cast() + + private val alice15Color = RgbColor(189, 206, 233) + private val aliceNullColor = RgbColor(233, 199, 220) + + private fun aliceColor(name: String, age: Int?): RgbColor? = + when (name to age) { + "Alice" to 15 -> alice15Color + "Alice" to null -> aliceNullColor + else -> null + } + + @Test + fun valueCountsDf() { + // SampleStart + df + // SampleEnd + .format().perRowCol { row, _ -> + aliceColor(row.name, row.age)?.let { + background(it) and textColor(black) + } + } + .saveDfHtmlSample() + } + + @Test + fun valueCounts() { + // SampleStart + df.valueCounts() + // SampleEnd + .format().perRowCol { row, _ -> + aliceColor(row.name, row.age)?.let { + background(it) and textColor(black) + } + } + .saveDfHtmlSample() + } + + @Test + fun valueCountsWithNA() { + // SampleStart + df.valueCounts(dropNA = false) + // SampleEnd + .format().perRowCol { row, _ -> + aliceColor(row.name, row.age)?.let { + background(it) and textColor(black) + } + } + .saveDfHtmlSample() + } + + @Test + fun valueCountsSelector_properties() { + // SampleStart + df.valueCounts(dropNA = false) { name } + // SampleEnd + .defaultHeaderFormatting { name } + .saveDfHtmlSample() + } + + @Test + fun valueCountsSelector_strings() { + // SampleStart + df.valueCounts("name", dropNA = false) + // SampleEnd + } + + @Test + fun valueCountsColumn() { + // SampleStart + df.age.valueCounts() + // SampleEnd + .defaultHeaderFormatting { "age"() } + .saveDfHtmlSample() + } +}