Repository: pyjanitor-devs/pyjanitor

Issue:
Truncate long column names ?
# Brief Description

Often when exporting CSVs from questionnaires, the column names are paragraphs, or really long strings.

I would like to propose that we add an option to `clean_names` that truncates long sentence column names to something shorter. This would be useful not just in looking at the df in a notebook, but also when renaming it. Currently my workaround is to print the column names, and then use a dictionary with existing:new column names that gets passed into the rename column method.

# Example API

```python
cleaned_df = (
    pd.read_csv('data/Learning Log #1 Survey Student Analysis Report.csv')
    .clean_names(remove_special = True, truncate_limit = 50)
    .remove_empty()
)
```

^ the tuncate_limit can be defaulted to None to avoid truncating, and a number enables the maximum number of characters in the column.



Discussion:
@firasm thank you for chiming in! Yes, this sounds like a neat thing to add in.

Would you like to take a stab at a contribution? We’d love to expand the contributor community, and have you join in too. If not, no worries, we’ll see who in the dev team might take interest here :smile:.
Thanks @ericmjl - unfortunately I don't have time to start looking at the source code (full teaching load this term!), but I can paste in my solution in the hopes that it gives someone a head start?

It's basically just a one-liner `.rename_columns({item: str(item)[0:8] for item in df.clean_names().columns})` where 8 is the number of chars that should be kept in each column name.

```
import janitor
import pandas as pd

company_sales = {
    'SalesMonth': ['Jan', 'Feb', 'Mar', 'April'],
    'Company1 - this is a really long column that has mostly useless stuff.': [150.0, 200.0, 300.0, 400.0],
    'Company2 - another exceptionally long column that has mostly useless stuff.': [180.0, 250.0, np.nan, 500.0],
    'Company3 - and finally a third column with mostly useless stuff.': [400.0, 500.0, 600.0, 675.0],
    'col1 to remove': [400.0, 500.0, 600.0, 675.0],
    'col2 to remove': [400.0, 500.0, 600.0, 675.0]
}

df = pd.DataFrame.from_dict(company_sales)

df
```
<img width="598" alt="Screen Shot 2020-09-20 at 10 45 19 AM" src="https://user-images.githubusercontent.com/2507459/93718059-78aea400-fb2e-11ea-9111-6d520e702b6a.png">

```
cleaned_df = (
    df.clean_names()
    .rename_columns({item: str(item)[0:8] for item in df.clean_names().columns})
    .remove_empty()
    .remove_columns(
        [
            "col1_to_", #would be nice if remove_columns could accept wildcard/regexp so I could do 'col1.*' ?
            "col2_to_"
        ]
    )
    .rename_columns(
        {
            "salesmon": "Month",
            "company1": "Apple",
            "company2": "Microsoft",
            "company3": "Google",
        }
    )
)

cleaned_df
```
<img width="324" alt="Screen Shot 2020-09-20 at 10 45 24 AM" src="https://user-images.githubusercontent.com/2507459/93718067-7e0bee80-fb2e-11ea-81d3-264c4478105f.png">


No worries, @firasm, totally understand! Thanks for chiming back with your implementation. We'll make sure to credit you with the implementation 😄! 

By the way, I have a feeling we've overlapped at UBC before, is this correct? I was originally an MBIM student, but eventually finished in ISCI.
Yes we must have! Your name/picture sounded familiar but I couldn't figure out exactly where I'd seen you. small world - very nice.

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -242,6 +242,7 @@ def clean_names(
     strip_accents: bool = True,
     preserve_original_columns: bool = True,
     enforce_string: bool = True,
+    truncate_limit: int = None,
 ) -> pd.DataFrame:
     """
     Clean column names.
@@ -294,6 +295,8 @@ def clean_names(
     :param enforce_string: Whether or not to convert all column names
         to string type. Defaults to True, but can be turned off.
         Columns with >1 levels will not be converted by default.
+    :param truncate_limit: (optional) Truncates formatted column names to
+        the specified length. Default None does not truncate.
     :returns: A pandas DataFrame.
     """
     original_column_names = list(df.columns)
@@ -314,6 +317,8 @@ def clean_names(
     df = df.rename(columns=lambda x: re.sub("_+", "_", x))  # noqa: PD005
     df = _strip_underscores(df, strip_underscores)
 
+    df = df.rename(columns=lambda x: x[:truncate_limit])
+
     # Store the original column names, if enabled by user
     if preserve_original_columns:
         df.__dict__["original_columns"] = original_column_names


Test patch:
diff --git a/tests/functions/test_clean_names.py b/tests/functions/test_clean_names.py
--- a/tests/functions/test_clean_names.py
+++ b/tests/functions/test_clean_names.py
@@ -167,3 +167,10 @@ def test_clean_names_enforce_string(dataframe):
     df = dataframe.rename(columns={"a": 1}).clean_names(enforce_string=True)
     for c in df.columns:
         assert isinstance(c, str)
+
+
+@pytest.mark.functions
+def test_clean_names_truncate_limit(dataframe):
+    df = dataframe.clean_names(truncate_limit=7)
+    expected_columns = ["a", "bell_ch", "decorat", "animals", "cities"]
+    assert set(df.columns) == set(expected_columns)