ENH: Adding function to easily generate dummy dataframes in a controlled manner.
State: closed

- [x] tests added / passed
- [x] Ensure all linting tests pass, see [here](https://pandas.pydata.org/pandas-docs/dev/development/contributing.html#code-standards) for how to run them
- [x] whatsnew entry

#### What's new
`generate_fake_dataframe` aims to facilitate the creation of dummy DataFrames with a specific configuration and datatypes. 
Current solutions like `makeDataFrame` or `makeMixedDataFrame` are not versatile enough and I think there is a need for something better. 

The idea is that with a short function call like `pd.generate_fake_dataframe(1000, "iiifccd")` you can generate a DataFrame with 1000 rows and, for example, 4 columns of integers, 1 of floats, 2 categorical columns and 1 with dates  in that specific order. Other params like `seed` to control randomization, or `intervals` to control the values among which data is sampled let us easily generate a DataFrame that is suited to our needs.

#### Checklist:
* I have followed the documentation as thoroughly as I could and added a long docstring with description, parameters, examples , etc.
* I have added a couple of tests.

#### Some concerns
* I put the function inside a new file called `generate_fake_dataframe.py` in `pandas/core/` as I was unsure about where was the right place to put it.
* I am quite unsure about the suitability of the name of the function and/or its parameters.
* I have run linting tests, but could not run the other ones since the environment setup via docker defined [here](https://pandas.pydata.org/pandas-docs/dev/development/contributing_environment.html#creating-an-environment-using-docker) is not working for me. However, since I am not touching anything already written and I am only adding stuff nothing should break. My own tests are passing of course. 

That's it. I hope moderators find this function useful and after the necessary changes can be somehow included in the library.

Labels:
name: Testing
description: pandas testing functions or related to the test suite

Comments:
@jreback I have taken away the import line from `pandas/__init__.py` so it is no longer a public function 
```python3
from pandas.core.generate_fake_dataframe import generate_fake_dataframe
```

The use case of `generate_fake_dataframe` is to quickly create dummy dataframes which can be used to test a piece of code, a statistical model, etc. It has a couple of advantages over `makeCustomDataFrame`:
* It gives easy control over the types of the columns, just by passing a string of characters representing them. It allows generation of columns of different types. Doing this with `makeCustomDataFrame` is more complicated, since you need to pass a custom function which generates values depending on (column, row).
* Function parameters are few and easy to grasp. Instead `makeCustomDataFrame` has a ton of parameters that are not very intuitive for me. 
* Generated DataFrames are more "friendly"  with `generate_fake_dataframe`. `makeCustomDataFrame`  creates a hierarchy for column names and indexes which is not simple. 
* We can pass (start, end) intervals to columns to specify the range of values from which data is going to be sampled. This allows us to emulate 'real' data. For example, if you know you are calling an integer column "age" you can set an interval as (0, 85) and get meaningful data.

All these points come from my personal, biased opinion of course.

A couple of examples to clarify:
```python3
# dfs can be generated simply as
df0 = generate_fake_dataframe(10, "ifcccfd")

# custom column_names can be passed, a seed for reproducibility, and a dictionary setting the intervals for dtypes
df1 = generate_fake_dataframe(
  size = 10, 
  cols = "cccfd", 
  col_names=["name", "pet", "city","height", "birthday"],
  intervals = {"f" : (1.72,1.95), "d" : ("1996-01-01","1996-12-31")},
  seed=42)

# intervals accepts also a list if specific intervals want to be set to specific columns.
# Categorical columns can be chosen among 4 families which are hardcoded into the function. 
df2 = generate_fake_dataframe(
  size = 30, 
  cols = "cicffcd", 
  col_names = ["user", "age", "residence", "weight","height", "pet", "registered"],
  intervals = [("names",15), (18,25),("cities", 15), (73.2,95.0),
               (1.65,1.95), ("animals", 11), None],
  seed = 30)

# lists with custom categories can be passed for categorical columns insted
df3 = generate_fake_dataframe(
          size = 20,
          cols = "cfcc",
          col_names = ["runner", "total_time", "skill", "adult"],
          intervals = [("names", 15),
                       (70,100),
                       ["stamina", "endurance", "speed"],
                       [True, False],
                      ])

df4 = generate_fake_dataframe(
          size = 20,
          cols = "ifccc",
          col_names = ["id", "target", "one_hot_1", "one_hot_2", "one_hot_3"],
          intervals = {"c" : [0,1]})

```
<img width="792" alt="df0" src="https://user-images.githubusercontent.com/11667449/127472144-fd10d98a-8986-4ffc-be92-3e8f04fff0bb.png">
<img width="990" alt="df1_df2" src="https://user-images.githubusercontent.com/11667449/127472994-554b0884-53f0-4b9d-9f61-9dcafa3cfb0c.png">
<img width="788" alt="df3_df4" src="https://user-images.githubusercontent.com/11667449/127473001-932b7469-e349-41ed-a4fb-208d55115a81.png">

Personally, I would not replace `makeCustomDataFrame`  since it has functionality that `generate_fake_dataframe` does not provide (defining index dtypes, etc) and some tests would probably break.
Instead, I believe the two functions could coexist since they serve slightly different purposes.  I see  `generate_fake_dataframe` more as a useful tool that everyone could use. That is why I tried to add it as a public function.  You mention that making it public is 'a separate decision'. I am not familiar with how the whole process works to present it as 'candidate' for a public function.  I would appreciate any help provided. 
I would also be -1 for this being a public functions directly, but also see some usefulness for a testing.

I think what a "dummy" DataFrame is a fuzzy concept and can have a broad scope (row/column size, data types, nulls, sortedness, randomness, etc.), and I don't think it's pandas place to be opinionated on how to create that type of data via a public function. 
@mroeschke thanks for your input. Where within the library do you think this function could possibly be placed? What about the naming of the function and/or arguments? Do you have any suggestions on that matter?
> I would also be -1 for this being a public functions directly, but also see some usefulness for a testing.
> 
> I think what a "dummy" DataFrame is a fuzzy concept and can have a broad scope (row/column size, data types, nulls, sortedness, randomness, etc.), and I don't think it's pandas place to be opinionated on how to create that type of data via a public function.

I agree, and am -1 on this. `pandas` core purpose is to be a package for users to create and manipulate data in dataframes. I think testing constructs (designed for the public) are better left to the public developers themselves using `pandas` primitives. As you have shown it is not difficult for such a method and it is unwise to expand the pandas toolbox with other items that need development and maintenance.
thanks @juanluisrto i think this would belong in a new package, possibly could reference this in the ecosystem guide.
Apologies for commenting on a closed ticket.  

@juanluisrto It would be great if you linked a repo/gist at the end of this thread so that folks wanting to use this functionality could 'grab it'.

Anyway, thanks for this super nice  dataframe generator :)
Glad you liked it! @brifordwylie !
[Here](https://gist.github.com/juanluisrto/ba842d2a0991244adc3f77d0278a9613) you have the gist. This is a shortened version not compliant with PEP-8 and without the documentation. 
[Here](https://github.com/juanluisrto/pandas/blob/generate_fake_df/pandas/core/generate_fake_dataframe.py) you have the version which I pull requested, documented and compliant.
Or you can just copy the code directly (this wont reflect any updates of course).
```python3
import pandas as pd
import numpy as np
from itertools import cycle
def generate_fake_dataframe(size, cols, col_names = None, intervals = None, seed = None):
    
    categories_dict = {'animals': ['cow', 'rabbit', 'duck', 'shrimp', 'pig', 'goat', 'crab', 'deer', 'bee', 'sheep', 'fish', 'turkey', 'dove', 'chicken', 'horse'],
                       'names'  : ['James', 'Mary', 'Robert', 'Patricia', 'John', 'Jennifer', 'Michael', 'Linda', 'William', 'Elizabeth', 'Ahmed', 'Barbara', 'Richard', 'Susan', 'Salomon', 'Juan Luis'],
                       'cities' : ['Stockholm', 'Denver', 'Moscow', 'Marseille', 'Palermo', 'Tokyo', 'Lisbon', 'Oslo', 'Nairobi', 'Río de Janeiro', 'Berlin', 'Bogotá', 'Manila', 'Madrid', 'Milwaukee'],
                       'colors' : ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'purple', 'pink', 'silver', 'gold', 'beige', 'brown', 'grey', 'black', 'white']
                      }
    default_intervals = {"i" : (0,10), "f" : (0,100), "c" : ("names", 5), "d" : ("2020-01-01","2020-12-31")}
    rng = np.random.default_rng(seed)

    first_c = default_intervals["c"][0]
    categories_names = cycle([first_c] + [c for c in categories_dict.keys() if c != first_c])
    default_intervals["c"] = (categories_names, default_intervals["c"][1])
    
    if isinstance(col_names,list):
        assert len(col_names) == len(cols), f"The fake DataFrame should have {len(cols)} columns but col_names is a list with {len(col_names)} elements"
    elif col_names is None:
        suffix = {"c" : "cat", "i" : "int", "f" : "float", "d" : "date"}
        col_names = [f"column_{str(i)}_{suffix.get(col)}" for i, col in enumerate(cols)]

    if isinstance(intervals,list):
        assert len(intervals) == len(cols), f"The fake DataFrame should have {len(cols)} columns but intervals is a list with {len(intervals)} elements"
    else:
        if isinstance(intervals,dict):
            assert len(set(intervals.keys()) - set(default_intervals.keys())) == 0, f"The intervals parameter has invalid keys"
            default_intervals.update(intervals)
        intervals = [default_intervals[col] for col in cols]
    df = pd.DataFrame()
    for col, col_name, interval in zip(cols, col_names, intervals):
        if interval is None:
            interval = default_intervals[col]
        assert (len(interval) == 2 and isinstance(interval, tuple)) or isinstance(interval, list), f"This interval {interval} is neither a tuple of two elements nor a list of strings."
        if col in ("i","f","d"):
            start, end = interval
        if col == "i":
            df[col_name] = rng.integers(start, end, size)
        elif col == "f":
            df[col_name] = rng.uniform(start, end, size)
        elif col == "c":
            if isinstance(interval, list):
                categories = np.array(interval)
            else:
                cat_family, length = interval
                if isinstance(cat_family, cycle):
                    cat_family = next(cat_family)
                assert cat_family in categories_dict.keys(), f"There are no samples for category '{cat_family}'. Consider passing a list of samples or use one of the available categories: {categories_dict.keys()}"
                categories = rng.choice(categories_dict[cat_family], length, replace = False, shuffle = True)
            df[col_name] = rng.choice(categories, size, shuffle = True)
        elif col == "d":
            df[col_name] = rng.choice(pd.date_range(start, end), size)
    return df  
```