Repository: dask/dask
Pull request: Documentation of Limitation of Sample()
Adding a documentation of the limitation of the dataframe method ```sample()```
- [ x] Closes #8712
- [ ] Tests added / passed
- [ ] Passes `pre-commit run --all-files`


Problem statement:
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

Hints from issue 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.
I think that a future resolution should involve shifting from an implicit (frac) to an explicit (n) sample size parameter. Within a framework like dask where attributes like length are only calculated on demand, explicit sample size is more natural.

Commit message: Modifying accortding to comments

Patch to review:
diff --git a/dask/dataframe/core.py b/dask/dataframe/core.py
index dad710ed5ae..68e9435719a 100644
--- a/dask/dataframe/core.py
+++ b/dask/dataframe/core.py
@@ -1538,14 +1538,35 @@ def sample(self, n=None, frac=None, replace=False, random_state=None):
         n : int, optional
             Number of items to return is not supported by dask. Use frac
             instead.
-        frac : float, optional
+        frac : frac : float, optional
             Fraction of axis items to return.
+            .. warning::
+            Since `sample` is mapped to each partition, `frac` should be greater
+            or equal to the number of partitions divided by the length of the
+            DataFrame or Series object (see below).
         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
 
+        Notes
+        -----
+        If `frac` is less than the number of partitions divided by the length
+        of the object, you may see unexpected results. As an example:
+
+        >>> import pandas as pd
+        >>> import dask.dataframe as dd
+        >>> df = pd.DataFrame({'test': range(0, 100)})
+        >>> ddf = dd.from_pandas(df, npartitions=10)
+        >>> numerators = [1, 9, 10, 20]
+        >>> # expected length after sampling
+        >>> numerators
+        [1, 9, 10, 20]
+        >>> # result of sample
+        >>> [len(ddf.sample(frac=numerator/100)) for numerator in numerators]
+        [0, 10, 10, 20]
+
         See Also
         --------
         DataFrame.random_split


Code review comments:
diff_hunk: @@ -1538,14 +1538,35 @@ def sample(self, n=None, frac=None, replace=False, random_state=None):
         n : int, optional
             Number of items to return is not supported by dask. Use frac
             instead.
-        frac : float, optional
+        frac : frac : float, optional
path: dask/dataframe/core.py
text: @user1:
```suggestion
        frac : float, optional
```