Repository: ericmjl/pyjanitor (python)

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.


Pull request description:
[ENH] Added truncation to clean_names
<!-- Thank you for your PR!

BEFORE YOU CONTINUE! Please add the appropriate three-letter abbreviation to your title.

The abbreviations can be:
- [DOC]: Documentation fixes.
- [ENH]: Code contributions and new features.
- [TST]: Test-related contributions.
- [INF]: Infrastructure-related contributions.

Also, do not forget to tag the relevant issue here as well.

Finally, as commits come in, don't forget to regularly rebase!
-->

# PR Description

Please describe the changes proposed in the pull request:

- Addition of optional `truncate_limit` argument for `clean_names` as suggested in #753 

<!-- Doing so provides maintainers with context on what the PR is, and can help us more effectively review your PR. -->

<!-- Please also identify below which issue that has been raised that you are going to close. -->

**This PR resolves #753.**

<!-- As you go down the PR template, please feel free to delete sections that are irrelevant. -->

# PR Checklist

<!-- This checklist exists for newcomers who are not yet familiar with our requirements. If you are experienced with
the project, please feel free to delete this section. -->

Please ensure that you have done the following:

1. [x] PR in from a fork off your branch. Do not PR from `<your_username>`:`dev`, but rather from `<your_username>`:`<feature-branch_name>`.
<!-- Doing this helps us keep the commit history much cleaner than it would otherwise be. -->
2. [x] If you're not on the contributors list, add yourself to `AUTHORS.rst`.
<!-- We'd like to acknowledge your contributions! -->
3. [x] Add a line to `CHANGELOG.rst` under the latest version header (i.e. the one that is "on deck") describing the contribution.
    - Do use some discretion here; if there are multiple PRs that are related, keep them in a single line.

## Quick Check

To do a very quick check that everything is correct, follow these steps below:

- [x] Run the command `make check` from pyjanitor's top-level directory. This will automatically run:
    - black formatting
    - flake8 checking
    - running the test suite
    - docs build

Once done, please check off the check-box above.

If `make check` does not work for you, you can execute the commands listed in the Makefile individually.

## Code Changes

<!-- If you have not made code changes, please feel free to delete this section. -->

If you are adding code changes, please ensure the following:

- [x] Ensure that you have added tests.
- [x] Run all tests (`$ pytest .`) locally on your machine.
    - [x] Check to ensure that test coverage covers the lines of code that you have added.
    - [x] Ensure that all tests pass.

## Documentation Changes

<!-- If you have not made documentation changes, please feel free to delete this section. -->

If you are adding documentation changes, please ensure the following:

- [x] Build the docs locally.
- [x] View the docs to check that it renders correctly.

# Relevant Reviewers

<!-- Finally, please tag relevant maintainers to review. -->

Please tag maintainers to review.

- @ericmjl


Patch:
diff --git a/AUTHORS.rst b/AUTHORS.rst
index dc5b4ac..fa5ad2b 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -93,3 +93,4 @@ Contributors
 - `@UGuntupalli <https://github.com/UGuntupalli>`_ | `contributions < https://github.com/ericmjl/pyjanitor/issues?q=is%3Aclosed+mentions%3AUGuntupalli >`_
 - `@mphirke <https://github.com/mphirke>`_ | `contributions <https://github.com/ericmjl/pyjanitor/issues?q=is%3Aclosed+mentions%3Amphirke>`_
 - `@sauln <https://github.com/sauln>`_ | `contributions <https://github.com/ericmjl/pyjanitor/issues?q=is%3Aclosed+mentions%3Asauln`_
+- `@richardqiu <https://github.com/richardqiu>`_ | `contributions <https://github.com/ericmjl/pyjanitor/issues?q=is%3Aclosed+mentions%3Arichardqiu>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9735235..15189f8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,7 @@
 new version (on deck)
 =====================
 - [INF] Deploy ``darglint`` package for docstring linting. Issue #745. @loganthomas
+- [ENH] Added optional truncation to ``clean_names`` function. Issue #753. @richardqiu
 - [ENH] Added ``timeseries.flag_jumps()`` function. Issue #711. @loganthomas
 
 
diff --git a/janitor/functions.py b/janitor/functions.py
index 63cb29d..a2a61d7 100644
--- 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
index bdd9813..4e7cb47 100644
--- 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)