What you will be able to do
- Tell apart DataFrame.count(), functions.count(col) and GroupedData.count(), and predict how each one treats nulls
- Calculate a mean with functions.mean/avg through select or agg, and explain why the result column is named avg(...)
- Use approx_count_distinct, set its rsd parameter, and know when to use count_distinct instead
- Choose between describe() and summary(), and ask summary() for only the statistics you need
Key concept
Aggregate methods vs aggregate functions — Some aggregates are methods on the DataFrame itself, such as count(), describe() and summary(), and you call them directly. Others are functions in pyspark.sql.functions, such as count, mean and approx_count_distinct, which return a Column that you pass to select() or agg(). Most exam questions on this topic come down to which kind of call you have and what it returns.
1.count: three calls with the same name
The word count shows up in three places in PySpark, and each one returns something different. The simplest is the DataFrame method df.count(). It is an action that returns a plain Python int holding the number of rows. You get a number back, not a DataFrame or a Column.
df = spark.createDataFrame(
[(14, "Tom"), (23, "Alice"), (16, "Bob")], ["age", "name"])
df.count()
# 3The second is the aggregate function sf.count(col) from pyspark.sql.functions. It returns a Column, so you use it inside select() or agg(), and the result comes back as a one-row DataFrame. What you pass it changes the answer. Passing sf.expr("*") counts every row, and the output header is literally count(1). Passing a specific column counts only the rows where that column is not null.
from pyspark.sql import functions as sf
df = spark.createDataFrame([(None,), ("a",), ("b",), ("c",)], schema=["alphabets"])
df.select(sf.count(sf.expr("*"))).show()
+--------+
|count(1)|
+--------+
| 4|
+--------+On that same DataFrame, df.select(sf.count(df.alphabets)) returns 3. The documentation shows the same split across two columns: for rows (1, "apple"), (2, "banana"), (3, None), count(id) is 3 and count(fruit) is 2. The third form is GroupedData.count(), which you call after groupBy. It counts the records in each group and returns a DataFrame with a count column next to the grouping key.
| Call | Returns | What it counts |
|---|---|---|
| df.count() | int | Number of rows in the DataFrame |
| sf.count(sf.expr("*")) | Column (header count(1)) | All rows, nulls included |
| sf.count(df.alphabets) | Column (header count(alphabets)) | Non-null values in that column |
| df.groupBy(...).count() | DataFrame | Number of records for each group |
Checkpoint 1 of 8· Check yourself
A DataFrame has the rows (1, "apple"), (2, "banana") and (3, None) with columns id and fruit. What does df.select(sf.count(df.id), sf.count(df.fruit)) show?
count(col) counts the non-null values in each column on its own terms. id has no nulls, so it counts 3. fruit has one None, so it counts 2.
“Count non-null values in multiple columns”Source: docs.databricks.com
Checkpoint 2 of 8· Exam question
A data engineer needs the count of distinct values in a `user_id` column across tens of millions of rows. Exact precision is not required and query latency matters more than accuracy. Which line correctly fills the blank? ```python from pyspark.sql import functions as F result = df.select(F.________("user_id")).collect()[0][0] ```
Correct answer: B — `approx_count_distinct("user_id")`, which relies on the HyperLogLog++ algorithm to estimate cardinality within a bounded relative error, trading exact precision for a single-pass, low-memory computation.
- A. This produces the exact distinct count but requires shuffling and deduplicating the full dataset, which is the cost the scenario explicitly wants to avoid, so it is not the best fit even though the result would be correct.
- B. This is correct: the HyperLogLog++-based approximate estimator is designed for exactly this scenario, returning a cardinality estimate with a configurable relative error while avoiding the shuffle a fully exact computation would require.
- C. This counts non-null occurrences without removing duplicates, so it answers a different question than distinct cardinality and would overcount whenever the same `user_id` appears more than once.
- D. This returns an array of unique values rather than a scalar count, and collecting every distinct value to the driver is far more expensive than either an exact or approximate count.
- E. This is a conditional sum over non-null rows, which is mathematically equivalent to a plain count of non-null values, not a distinct count, so it does not solve the cardinality problem at all.
2.mean and avg, through select or agg
Counting tells you how many rows there are. Averaging tells you something about their values. pyspark.sql.functions has both avg(col) and mean(col), and they are the same function under two names. Because mean is an alias, Spark labels the output column avg(age) even when your code calls sf.mean("age"). If a question shows avg(...) in a result header, don't conclude that the code must have called avg.
import pyspark.sql.functions as sf
df = spark.createDataFrame([(1982, None), (1990, 2), (2000, 4)], ["birth", "age"])
df.select(sf.avg("age")).show()
+--------+
|avg(age)|
+--------+
| 3.0|
+--------+The answer is 3.0. Nulls are left out of both the sum and the divisor, so a null is not treated as zero. The Spark SQL reference for mean says the same thing and adds one edge case: if a group is empty or holds only nulls, the result is NULL.
So far the examples have used select(). The other common entry point is df.agg(), which aggregates over the whole DataFrame and is shorthand for df.groupBy().agg(). It accepts two forms. One is a dictionary that maps a column name to the name of an aggregate function. The other is one or more aggregate Column expressions, like the ones you have been building with sf.
from pyspark.sql import functions as sf
df = spark.createDataFrame([(2, "Alice"), (5, "Bob")], schema=["age", "name"])
df.agg({"age": "max"}).show()
# +--------+
# |max(age)|
# +--------+
# | 5|
# +--------+
df.agg(sf.min(df.age)).show()
# +--------+
# |min(age)|
# +--------+
# | 2|
# +--------+After a groupBy, the same alias appears as a shortcut method: GroupedData.mean(*cols) and GroupedData.avg(*cols) both compute the average of each numeric column for each group. For example, df.groupBy("name").avg("age") returns 2.5 for Alice (ages 2 and 3) and 7.5 for Bob (ages 5 and 10).
Checkpoint 3 of 8· Check yourself
You run df.select(sf.mean("age")).show(). What header does the result column have?
mean is an alias of avg, and the documented output of sf.mean("age") is labelled avg(age).
“Returns the average of the values in a group. An alias of avg.”Source: docs.databricks.com
3.approx_count_distinct: trading exactness for speed
count and mean always return exact answers. Counting distinct values exactly can be expensive on very large data, and Databricks names approximation as the way to make it cheaper. The function for this is sf.approx_count_distinct(col, rsd=None). It returns a Column that estimates the number of distinct values in a column. Like the other aggregate functions, you use it inside agg() or select().
| Parameter | Type | Meaning |
|---|---|---|
| col | pyspark.sql.Column or column name | The column whose distinct values are counted |
| rsd | float, optional | Maximum allowed relative standard deviation, default 0.05. Below 0.01, count_distinct is more efficient. |
Checkpoint 4 of 8· Fill the gap
Which function completes this sample so that it estimates the number of distinct fruits?
from pyspark.sql import functions as sf
df = spark.createDataFrame([("apple",), ("orange",), ("apple",), ("banana",)], ['fruit'])
df.agg(sf. ? ("fruit")).show()approx_count_distinct estimates how many distinct values the column has, which is 3 here. count would return 4, the number of non-null values.
Source: docs.databricks.comThe function counts distinct values in one column. To count distinct combinations across several columns, the documentation first packs them into a struct with sf.struct("name", "value") and passes the struct column in. For rows (Alice, 1), (Alice, 2), (Bob, 3), (Bob, 3), that returns 3. Small examples like these come back exact. On a larger input the estimate starts to drift from the true count.
from pyspark.sql import functions as sf
spark.range(100000).agg(
sf.approx_count_distinct("id").alias('with_default_rsd'),
sf.approx_count_distinct("id", 0.1).alias('with_rsd_0.1')
).show()
+----------------+------------+
|with_default_rsd|with_rsd_0.1|
+----------------+------------+
| 95546| 102065|
+----------------+------------+The default rsd of 0.05 lands at 95546, and the looser 0.1 lands at 102065. A larger rsd permits more error, and the estimate can fall above or below the true value. Lowering rsd makes the estimate tighter, but the documentation puts a floor on how far that is worth taking. Below 0.01, you are better off using the exact count_distinct.
Checkpoint 5 of 8· Check yourself
You need a distinct count with a relative standard deviation tighter than 0.01. What do the docs recommend?
Below 0.01, the exact count_distinct is more efficient than forcing the approximation to be that precise.
“If rsd < 0.01, it would be more efficient to use count_distinct.”Source: docs.databricks.com
Checkpoint 6 of 8· Exam question
A DataFrame `orders` has 1,000 rows, and its `discount_code` column is null for 300 of them. What does the following code return, and why? ```python orders.select(F.count("discount_code")).collect()[0][0] ```
Correct answer: D — 700, because `count` applied to a named column counts only the non-null values in that column, excluding the 300 rows where it is null.
- A. `count()` works perfectly well with a column name; the wildcard form is an alternative for counting all rows including nulls, not a requirement for the function to operate.
- B. Counting every row regardless of nulls is the behavior of `count("*")` or `df.count()`, not `count()` on a specific column, so this overstates the result.
- C. Counting a column reports how many values are present, not how many are missing, so this has the logic inverted and would undercount the actual result.
- D. This is correct: counting a named column skips rows where that column is null, so only the 700 rows with a non-null discount code are tallied.
- E. Aggregate functions in Spark are designed to handle nulls directly, so no preceding `dropna()` call is needed for `count` to execute successfully.
4.describe and summary: many statistics in one call
The functions above compute one statistic at a time. When you want a quick profile of a DataFrame, two DataFrame methods compute several at once: describe() and summary(). Both work on numeric and string columns, and both return a new DataFrame whose first column, summary, names the statistic in each row. Both are documented as tools for exploratory data analysis, and Databricks makes no promise that the layout of the result stays backward compatible. That makes them poor choices to feed into downstream pipeline code.
df.describe(['age']).show()
# +-------+----+
# |summary| age|
# +-------+----+
# | count| 3|
# | mean|12.0|
# | stddev| 1.0|
# | min| 11|
# | max| 13|
# +-------+----+describe(*cols) takes a column name or a list of column names and covers every column if you pass none. It always returns the same five statistics. summary(*statistics) works the other way round. You choose columns beforehand with select(), and its arguments name the statistics you want. Called with no arguments, it returns count, mean, stddev, min, the 25%, 50% and 75% percentiles, and max. You can also request arbitrary approximate percentiles written as percentages. The describe documentation points to summary when you need more statistics or want to choose which ones are computed.
df.select("age", "weight", "height").summary("count", "min", "25%", "75%", "max").show()
# +-------+---+------+------+
# |summary|age|weight|height|
# +-------+---+------+------+
# | count| 3| 3| 3|
# | min| 11| 37.8| 142.2|
# | 25%| 11| 37.8| 142.2|
# | 75%| 13| 44.1| 150.5|
# | max| 13| 44.1| 150.5|
# +-------+---+------+------+| Aspect | describe(*cols) | summary(*statistics) |
|---|---|---|
| Arguments name | Columns to describe | Statistics to compute |
| Default output | count, mean, stddev, min, max | count, mean, stddev, min, 25%, 50%, 75%, max |
| Percentiles | None | Any approximate percentile written as a percentage, such as 75% |
| Returns | DataFrame with a summary column | DataFrame with a summary column |
Checkpoint 7 of 8· Match them up
Match each call to what it produces
Tap a term, then the definition that fits it.
describe always returns the five basic statistics for the columns you name. summary returns the full set by default, including the quartiles, or only the statistics you ask for. count() returns a row count, not a statistics table.
“Use summary for expanded statistics and control over which statistics to compute.”Source: docs.databricks.com
Checkpoint 8 of 8· Exam question
A data engineer wants to compute, for each `region`, the number of orders and the average `order_total`, with the average column labeled `avg_total`. Which code produces this result?
Correct answer: C — ```python orders.groupBy("region").agg( F.count("order_id").alias("order_count"), F.avg("order_total").alias("avg_total") ) ``` which passes both named aggregates to `.agg()` after grouping, giving each output column its own alias.
- A. Calling `.agg()` first collapses the DataFrame into a single aggregated row before any grouping is applied, so chaining `.groupBy()` afterward has no grouping keys left to operate on and does not produce a per-region breakdown.
- B. `.count()` only returns a row count per group with a fixed column name, and calling `.alias()` on the resulting DataFrame does not rename a column or add an average, so this does not meet the requirement.
- C. This is correct: `.agg()` after `.groupBy()` accepts multiple named aggregate expressions, and each `.alias()` controls the output column label independently, producing the exact two-column report requested.
- D. `.mean()` on a grouped DataFrame names its own output column `avg(order_total)` and produces no `order_count` column, so renaming a nonexistent `order_count` column has no effect and the count is missing entirely.
- E. Mixing an unaggregated grouping key with aggregate expressions inside `.select()` and then calling `.groupBy()` afterward is not the pattern Spark expects; aggregate expressions must be resolved through `.agg()` alongside the grouping columns, not after a plain `.select()`.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.sf.count(df.col) counts every row in the DataFrame, the same way df.count() does.Why is that wrong?
A count on a named column skips nulls in that column. Only count("*"), shown as count(1), or the DataFrame method df.count() counts every row.
Covered in count: three calls with the same name
2.approx_count_distinct returns the exact distinct count, and you should keep lowering rsd for better results.Why is that wrong?
It returns an estimate: 95546 for 100,000 distinct ids at the default rsd. Below an rsd of 0.01, the exact count_distinct is the more efficient choice.
Covered in approx_count_distinct: trading exactness for speed
3.describe() and summary() are interchangeable, and both include percentiles.Why is that wrong?
describe() returns only count, mean, stddev, min and max. summary() adds percentiles and lets you choose which statistics to compute.
Covered in describe and summary: many statistics in one call
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“Returns the number of rows in this DataFrame.”
↩︎ count: three calls with the same name - 2.
“Count non-null values in a specific column”
↩︎ count: three calls with the same name“Count non-null values in a specific column”
↩︎ Exam trap 1“Count non-null values in multiple columns”
↩︎ Checkpoint - 3.
“Counts the number of records for each group.”
↩︎ count: three calls with the same name“Computes average values for each numeric column for each group. mean is an alias.”
↩︎ mean and avg, through select or agg - 4.
“Returns the average of the values in a group. An alias of avg.”
↩︎ mean and avg, through select or agg - 5.
“Nulls within the group are ignored. If a group is empty or consists only of nulls the result is NULL.”
↩︎ mean and avg, through select or agg - 6.
“Aggregate on the entire DataFrame without groups (shorthand for df.groupBy().agg()).”
↩︎ mean and avg, through select or agg - 7.
“using approximation for aggregates can accelerate query processing and reduce costs when you don't require precise results.”
↩︎ approx_count_distinct: trading exactness for speed - 8.
“The maximum allowed relative standard deviation (default = 0.05).”
↩︎ approx_count_distinct: trading exactness for speed“If rsd < 0.01, it would be more efficient to use count_distinct.”
↩︎ approx_count_distinct: trading exactness for speed“If rsd < 0.01, it would be more efficient to use count_distinct.”
↩︎ Exam trap 2“estimates the approximate distinct count of elements in a specified column or a group of columns.”
↩︎ Prediction - 9.
“Computes basic statistics for numeric and string columns.”
↩︎ describe and summary: many statistics in one call“Use summary for expanded statistics and control over which statistics to compute.”
↩︎ describe and summary: many statistics in one call - 10.
“This function is meant for exploratory data analysis”
↩︎ describe and summary: many statistics in one call“Available statistics are: count, mean, stddev, min, max, arbitrary approximate percentiles specified as a percentage (e.g., 75%).”
↩︎ Exam trap 3
Also cited
- https://docs.databricks.com/aws/en/pyspark/basicsOfficial docs
“Some aggregations are actions, which means that they trigger computations.”
↩︎ Key concept