Repository: dask/dask

Issue:
Sampling from Dataframe for frac < npartitions / nrows
**What happened**:
I have been trying to sample rows from a dataframe using `sample(frac)` and realised that it does only work properly if the parameter `frac >= npartitions / nrows`, where `nrows` is the number of rows of the dataframe. For example, for `frac = 1 / nrows` `sample(frac)` returns a sample of `sample size = npartitions` instead of 1.
 
**What you expected to happen**:
I expected sample size to be `frac * nrows` . For example, if `frac = 1 / nrows`, then `sample size = 1`.

**Minimal Complete Verifiable Example**:
```python
import pandas as pd
import dask.dataframe as dd

df = dd.from_pandas(pd.DataFrame({'test':range(0,100)}), npartitions= 10)
print(f'Expectation | Result')
print(f'--------------------')
for numerator in (1, 5, 9, 10, 20, 30, 50):
    sample = df.sample(frac= numerator/100)
    print(f'{numerator:^11} | {len(sample):^7}')
```


**Environment**:

- Dask version: 2022.1.1
- Python version: 3.8.9
- Operating System: MacOS
- Install method (conda, pip, source): pipenv




Discussion:
Thanks for writing this up so well! That is a pretty compelling example. I am taking a look at the sample code now to see if I can tell what's going on.

Ok so this makes sense. Dask just maps the `sample(frac)` to each partition so we get a little bit of rounding differences. I found this example illustrative:

```python
sum([len(p.compute().sample(frac=1/100)) for p in df.partitions])  # 0
```

I think the question becomes: Is this something to solve with documentation or with code changes? 

It is hard for me to think about how we could do it with code changes since `nrows` isn't something that we know until compute. 

It does seem like we could support a pseudo random sample using `n` rather than frac by splitting `n` into a list of integers with something like in this [stack overflow post](https://stackoverflow.com/questions/49666779/generating-random-numbers-to-obtain-a-fixed-sumpython/49666871).

Sorry, for the delay.

> I think the question becomes: Is this something to solve with documentation or with code changes?

Ideally, dask dataframe provides the same functionality (and similar results) as pandas dataframe. This is why I would prefer a code change. Of course, if it is not possible it requires documentation.

> It does seem like we could support a pseudo random sample using n rather than frac by splitting n into a list of integers with something like in this [stack overflow post](https://stackoverflow.com/questions/49666779/generating-random-numbers-to-obtain-a-fixed-sumpython/49666871).

I don't have any preference when it comes to `frac` vs `n`. If switching from `frac` to `n` solves the problem, I vote for using `n`.
Yeah. I think it's actually a surprisingly hard problem, but I think it would benefit from #7933 so maybe short-term we do a docs fix so that at least the behavior is clear while we try to figure out the right approach.
Thanks for looking into this, Julia! 

@sissnad Would you be interested in opening a PR to update the docs? 
Yes, sure. Do you have any documentation guidelines?
We have development guidelines that explain _how_ to write and build the docs (https://docs.dask.org/en/stable/develop.html), but I don't think we have style guidelines as much. Probably in this case just adding a few lines to the docstring would be sufficient.

Resolving patch:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -1539,12 +1539,17 @@ def sample(self, n=None, frac=None, replace=False, random_state=None):
             Number of items to return is not supported by dask. Use frac
             instead.
         frac : float, optional
-            Fraction of axis items to return.
+            Approximate fraction of items to return. This sampling fraction is
+            applied to all partitions equally. Note that this is an
+            **approximate fraction**. You should not expect exactly ``len(df) * frac``
+            items to be returned, as the exact number of elements selected will
+            depend on how your data is partitioned (but should be pretty close
+            in practice).
         replace : boolean, optional
             Sample with or without replacement. Default = False.
         random_state : int or ``np.random.RandomState``
-            If int we create a new RandomState with this as the seed
-            Otherwise we draw from the passed RandomState
+            If an int, we create a new RandomState with this as the seed;
+            Otherwise we draw from the passed RandomState.
 
         See Also
         --------


Test patch: