We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Remove Trailing (and Leading) underscores from clean_names
Pandas handles this relatively well but would be a good as a kwarg for `jn.clean_names()`.

The default `False` or `None` could leave leading, `True` or `'both'` to remove all and trailing and then pass `'leading'` and `'trailing`' or similar to remove each.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `janitor/functions.py`
Content:
```
1 import datetime as dt
2 from functools import reduce
3 
4 import pandas as pd
5 
6 from .errors import JanitorError
7 
8 import re
9 
10 
11 def clean_names(df):
12     """
13     Clean column names.
14 
15     Takes all column names, converts them to lowercase, then replaces all
16     spaces with underscores.
17 
18     Functional usage example:
19 
20     .. code-block:: python
21 
22         df = clean_names(df)
23 
24     Method chaining example:
25 
26     .. code-block:: python
27 
28         df = pd.DataFrame(...)
29         df = jn.DataFrame(df).clean_names()
30 
31     :param df: The pandas DataFrame object.
32     :returns: A pandas DataFrame.
33     """
34     df = df.rename(
35         columns=lambda x: x.lower()
36                            .replace(' ', '_')
37                            .replace('/', '_')
38                            .replace(':', '_')
39                            .replace("'", '')
40                            .replace('’', '')
41                            .replace(',', '_')
42                            .replace('?', '_')
43                            .replace('-', '_')
44                            .replace('(', '_')
45                            .replace(')', '_')
46                            .replace('.', '_')
47     )
48 
49     df = df.rename(columns=lambda x: re.sub('_+', '_', x))
50     return df
51 
52 
53 def remove_empty(df):
54     """
55     Drop all rows and columns that are completely null.
56 
57     Implementation is shamelessly copied from `StackOverflow`_.
58 
59     .. _StackOverflow: https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan  # noqa: E501
60 
61     Functional usage example:
62 
63     .. code-block:: python
64 
65         df = remove_empty(df)
66 
67     Method chaining example:
68 
69     .. code-block:: python
70 
71         df = pd.DataFrame(...)
72         df = jn.DataFrame(df).remove_empty()
73 
74     :param df: The pandas DataFrame object.
75     :returns: A pandas DataFrame.
76     """
77 
78     nanrows = df.index[df.isnull().all(axis=1)]
79     df.drop(index=nanrows, inplace=True)
80 
81     nancols = df.columns[df.isnull().all(axis=0)]
82     df.drop(columns=nancols, inplace=True)
83 
84     return df
85 
86 
87 def get_dupes(df, columns=None):
88     """
89     Returns all duplicate rows.
90 
91     Functional usage example:
92 
93     .. code-block:: python
94 
95         get_dupes(df)
96 
97     Method chaining example:
98 
99     .. code-block:: python
100 
101         df = pd.DataFrame(...)
102         jn.DataFrame(df).get_dupes()
103 
104     :param df: The pandas DataFrame object.
105     :param str/iterable columns: (optional) A column name or an iterable (list
106         or tuple) of column names. Following pandas API, this only considers
107         certain columns for identifying duplicates. Defaults to using all
108         columns.
109     :returns: The duplicate rows, as a pandas DataFrame.
110     """
111     dupes = df.duplicated(subset=columns, keep=False)
112     return df[dupes == True]  # noqa: E712
113 
114 
115 def encode_categorical(df, columns):
116     """
117     Encode the specified columns as categorical.
118 
119     Functional usage example:
120 
121     .. code-block:: python
122 
123         encode_categorical(df, columns="my_categorical_column")  # one way
124 
125     Method chaining example:
126 
127     .. code-block:: python
128 
129         df = pd.DataFrame(...)
130         categorical_cols = ['col1', 'col2', 'col4']
131         jn.DataFrame(df).encode_categorical(columns=categorical_cols)
132 
133     :param df: The pandas DataFrame object.
134     :param str/iterable columns: A column name or an iterable (list or tuple)
135         of column names.
136     :returns: A pandas DataFrame
137     """
138     if isinstance(columns, list) or isinstance(columns, tuple):
139         for col in columns:
140             assert col in df.columns, \
141                 JanitorError("{col} missing from dataframe columns!".format(col=col))  # noqa: E501
142             df[col] = pd.Categorical(df[col])
143     elif isinstance(columns, str):
144         df[columns] = pd.Categorical(df[columns])
145     else:
146         raise JanitorError('kwarg `columns` must be a string or iterable!')
147     return df
148 
149 
150 def get_features_targets(df, target_columns, feature_columns=None):
151     """
152     Get the features and targets as separate DataFrames/Series.
153 
154     The behaviour is as such:
155 
156     - `target_columns` is mandatory.
157     - If `feature_columns` is present, then we will respect the column names
158       inside there.
159     - If `feature_columns` is not passed in, then we will assume that the
160       rest of the columns are feature columns, and return them.
161 
162     Functional usage example:
163 
164     .. code-block:: python
165 
166         X, y = get_features_targets(df, target_columns="measurement")
167 
168     Method chaining example:
169 
170     .. code-block:: python
171 
172         df = pd.DataFrame(...)
173         target_cols = ['output1', 'output2']
174         X, y = jn.DataFrame(df).get_features_targets(target_columns=target_cols)  # noqa: E501
175 
176     :param df: The pandas DataFrame object.
177     :param str/iterable target_columns: Either a column name or an iterable
178         (list or tuple) of column names that are the target(s) to be predicted.
179     :param str/iterable feature_columns: (optional) The column name or iterable
180         of column names that are the features (a.k.a. predictors) used to
181         predict the targets.
182     :returns: (X, Y) the feature matrix (X) and the target matrix (Y). Both are
183         pandas DataFrames.
184     """
185     Y = df[target_columns]
186 
187     if feature_columns:
188         X = df[feature_columns]
189     else:
190         if isinstance(target_columns, str):
191             xcols = [c for c in df.columns if target_columns != c]
192         elif (isinstance(target_columns, list)
193                 or isinstance(target_columns, tuple)):
194             xcols = [c for c in df.columns if c not in target_columns]
195         X = df[xcols]
196     return X, Y
197 
198 
199 def rename_column(df, old, new):
200     """
201     Rename a column in place.
202 
203     Functional usage example:
204 
205     .. code-block:: python
206 
207         df = rename_column("old_column_name", "new_column_name")
208 
209     Method chaining example:
210 
211     .. code-block:: python
212 
213         df = pd.DataFrame(...)
214         df = jn.DataFrame(df).rename_column("old_column_name", "new_column_name")  # noqa: E501
215 
216     This is just syntactic sugar/a convenience function for renaming one column
217     at a time. If you are convinced that there are multiple columns in need of
218     changing, then use the :py:meth:`pandas.DataFrame.rename` method.
219 
220     :param str old: The old column name.
221     :param str new: The new column name.
222     :returns: A pandas DataFrame.
223     """
224     return df.rename(columns={old: new})
225 
226 
227 def coalesce(df, columns, new_column_name):
228     """
229     Coalesces two or more columns of data in order of column names provided.
230 
231     Functional usage example:
232 
233     .. code-block:: python
234 
235         df = coalesce(df, columns=['col1', 'col2'])
236 
237     Method chaining example:
238 
239     .. code-block:: python
240 
241         df = pd.DataFrame(...)
242         df = jn.DataFrame(df).coalesce(['col1', 'col2'])
243 
244 
245     The result of this function is that we take the first non-null value across
246     rows.
247 
248     This is more syntactic diabetes! For R users, this should look familiar to
249     `dplyr`'s `coalesce` function; for Python users, the interface
250     should be more intuitive than the :py:meth:`pandas.Series.combine_first`
251     method (which we're just using internally anyways).
252 
253     :param df: A pandas DataFrame.
254     :param columns: A list of column names.
255     :param str new_column_name: The new column name after combining.
256     :returns: A pandas DataFrame.
257     """
258     series = [df[c] for c in columns]
259 
260     def _coalesce(series1, series2):
261         return series1.combine_first(series2)
262     df = df.drop(columns=columns)
263     df[new_column_name] = reduce(_coalesce, series)  # noqa: F821
264     return df
265 
266 
267 def convert_excel_date(df, column):
268     """
269     Convert Excel's serial date format into Python datetime format.
270 
271     Implementation is also from `Stack Overflow`.
272 
273     .. _Stack Overflow: https://stackoverflow.com/questions/38454403/convert-excel-style-date-with-pandas  # noqa: E501
274 
275     Functional usage example:
276 
277     .. code-block:: python
278 
279         df = convert_excel_date(df, column='date')
280 
281     Method chaining example:
282 
283     .. code-block:: python
284 
285         df = pd.DataFrame(...)
286         df = jn.DataFrame(df).convert_excel_date('date')
287 
288     :param df: A pandas DataFrame.
289     :param str column: A column name.
290     :returns: A pandas DataFrame with corrected dates.
291     """
292     df[column] = (pd.TimedeltaIndex(df[column], unit='d')
293                   + dt.datetime(1899, 12, 30))
294     return df
295 
296 
297 def fill_empty(df, columns, value):
298     """
299     Fill `NaN` values in specified columns with a given value.
300 
301     Super sugary syntax that wraps :py:meth:`pandas.DataFrame.fillna`.
302 
303     Functional usage example:
304 
305     .. code-block:: python
306 
307         df = fill_empty(df, columns=['col1', 'col2'], value=0)
308 
309     Method chaining example:
310 
311     .. code-block:: python
312 
313         df = pd.DataFrame(...)
314         df = jn.DataFrame(df).fill_empty(df, columns='col1', value=0)
315 
316     :param df: A pandas DataFrame.
317     :param columns: Either a `str` or `list` or `tuple`. If a string is passed
318         in, then only that column will be filled; if a list or tuple of strings
319         are passed in, then they will all be filled with the same value.
320     :param value: The value that replaces the `NaN` values.
321     """
322     if isinstance(columns, list) or isinstance(columns, tuple):
323         for col in columns:
324             assert col in df.columns, \
325                 JanitorError("{col} missing from dataframe columns!".format(col=col))  # noqa: E501
326             df[col] = df[col].fillna(value)
327     elif isinstance(columns, str):
328         df[columns] = df[columns].fillna(value)
329     else:
330         raise JanitorError('kwarg `columns` must be a string or iterable!')
331 
332     return df
333
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -8,7 +8,38 @@
 import re
 
 
-def clean_names(df):
+def _strip_underscores(df, strip_underscores=None):
+    """
+    Strip underscores from the beginning, end or both of the
+    of the DataFrames column names.
+
+    .. code-block:: python
+
+        df = _strip_underscores(df, strip_underscores='left')
+
+    :param df: The pandas DataFrame object.
+    :param strip_underscores: (optional) Removes the outer underscores from all
+        column names. Default None keeps outer underscores. Values can be
+        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'
+        and True.
+    :returns: A pandas DataFrame.
+    """
+    underscore_options = [None, 'left', 'right', 'both', 'l', 'r', True]
+    if strip_underscores not in underscore_options:
+        raise JanitorError(
+            """strip_underscores must be one of: %s""" % underscore_options
+            )
+
+    if strip_underscores in ['left', 'l']:
+        df = df.rename(columns=lambda x: x.lstrip('_'))
+    elif strip_underscores in ['right', 'r']:
+        df = df.rename(columns=lambda x: x.rstrip('_'))
+    elif strip_underscores == 'both' or strip_underscores is True:
+        df = df.rename(columns=lambda x: x.strip('_'))
+    return df
+
+
+def clean_names(df, strip_underscores=None):
     """
     Clean column names.
 
@@ -29,6 +60,10 @@
         df = jn.DataFrame(df).clean_names()
 
     :param df: The pandas DataFrame object.
+    :param strip_underscores: (optional) Removes the outer underscores from all
+        column names. Default None keeps outer underscores. Values can be
+        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'
+        and True.
     :returns: A pandas DataFrame.
     """
     df = df.rename(
@@ -47,6 +82,7 @@
     )
 
     df = df.rename(columns=lambda x: re.sub('_+', '_', x))
+    df = _strip_underscores(df, strip_underscores)
     return df
 
 
@@ -190,7 +226,7 @@
         if isinstance(target_columns, str):
             xcols = [c for c in df.columns if target_columns != c]
         elif (isinstance(target_columns, list)
-                or isinstance(target_columns, tuple)):
+              or isinstance(target_columns, tuple)):
             xcols = [c for c in df.columns if c not in target_columns]
         X = df[xcols]
     return X, Y

{"golden_diff": "diff --git a/janitor/functions.py b/janitor/functions.py\n--- a/janitor/functions.py\n+++ b/janitor/functions.py\n@@ -8,7 +8,38 @@\n import re\n \n \n-def clean_names(df):\n+def _strip_underscores(df, strip_underscores=None):\n+    \"\"\"\n+    Strip underscores from the beginning, end or both of the\n+    of the DataFrames column names.\n+\n+    .. code-block:: python\n+\n+        df = _strip_underscores(df, strip_underscores='left')\n+\n+    :param df: The pandas DataFrame object.\n+    :param strip_underscores: (optional) Removes the outer underscores from all\n+        column names. Default None keeps outer underscores. Values can be\n+        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'\n+        and True.\n+    :returns: A pandas DataFrame.\n+    \"\"\"\n+    underscore_options = [None, 'left', 'right', 'both', 'l', 'r', True]\n+    if strip_underscores not in underscore_options:\n+        raise JanitorError(\n+            \"\"\"strip_underscores must be one of: %s\"\"\" % underscore_options\n+            )\n+\n+    if strip_underscores in ['left', 'l']:\n+        df = df.rename(columns=lambda x: x.lstrip('_'))\n+    elif strip_underscores in ['right', 'r']:\n+        df = df.rename(columns=lambda x: x.rstrip('_'))\n+    elif strip_underscores == 'both' or strip_underscores is True:\n+        df = df.rename(columns=lambda x: x.strip('_'))\n+    return df\n+\n+\n+def clean_names(df, strip_underscores=None):\n     \"\"\"\n     Clean column names.\n \n@@ -29,6 +60,10 @@\n         df = jn.DataFrame(df).clean_names()\n \n     :param df: The pandas DataFrame object.\n+    :param strip_underscores: (optional) Removes the outer underscores from all\n+        column names. Default None keeps outer underscores. Values can be\n+        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'\n+        and True.\n     :returns: A pandas DataFrame.\n     \"\"\"\n     df = df.rename(\n@@ -47,6 +82,7 @@\n     )\n \n     df = df.rename(columns=lambda x: re.sub('_+', '_', x))\n+    df = _strip_underscores(df, strip_underscores)\n     return df\n \n \n@@ -190,7 +226,7 @@\n         if isinstance(target_columns, str):\n             xcols = [c for c in df.columns if target_columns != c]\n         elif (isinstance(target_columns, list)\n-                or isinstance(target_columns, tuple)):\n+              or isinstance(target_columns, tuple)):\n             xcols = [c for c in df.columns if c not in target_columns]\n         X = df[xcols]\n     return X, Y\n", "issue": "Remove Trailing (and Leading) underscores from clean_names\nPandas handles this relatively well but would be a good as a kwarg for `jn.clean_names()`.\r\n\r\nThe default `False` or `None` could leave leading, `True` or `'both'` to remove all and trailing and then pass `'leading'` and `'trailing`' or similar to remove each.\n", "before_files": [{"content": "import datetime as dt\nfrom functools import reduce\n\nimport pandas as pd\n\nfrom .errors import JanitorError\n\nimport re\n\n\ndef clean_names(df):\n    \"\"\"\n    Clean column names.\n\n    Takes all column names, converts them to lowercase, then replaces all\n    spaces with underscores.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = clean_names(df)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).clean_names()\n\n    :param df: The pandas DataFrame object.\n    :returns: A pandas DataFrame.\n    \"\"\"\n    df = df.rename(\n        columns=lambda x: x.lower()\n                           .replace(' ', '_')\n                           .replace('/', '_')\n                           .replace(':', '_')\n                           .replace(\"'\", '')\n                           .replace('\u2019', '')\n                           .replace(',', '_')\n                           .replace('?', '_')\n                           .replace('-', '_')\n                           .replace('(', '_')\n                           .replace(')', '_')\n                           .replace('.', '_')\n    )\n\n    df = df.rename(columns=lambda x: re.sub('_+', '_', x))\n    return df\n\n\ndef remove_empty(df):\n    \"\"\"\n    Drop all rows and columns that are completely null.\n\n    Implementation is shamelessly copied from `StackOverflow`_.\n\n    .. _StackOverflow: https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan  # noqa: E501\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = remove_empty(df)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).remove_empty()\n\n    :param df: The pandas DataFrame object.\n    :returns: A pandas DataFrame.\n    \"\"\"\n\n    nanrows = df.index[df.isnull().all(axis=1)]\n    df.drop(index=nanrows, inplace=True)\n\n    nancols = df.columns[df.isnull().all(axis=0)]\n    df.drop(columns=nancols, inplace=True)\n\n    return df\n\n\ndef get_dupes(df, columns=None):\n    \"\"\"\n    Returns all duplicate rows.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        get_dupes(df)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        jn.DataFrame(df).get_dupes()\n\n    :param df: The pandas DataFrame object.\n    :param str/iterable columns: (optional) A column name or an iterable (list\n        or tuple) of column names. Following pandas API, this only considers\n        certain columns for identifying duplicates. Defaults to using all\n        columns.\n    :returns: The duplicate rows, as a pandas DataFrame.\n    \"\"\"\n    dupes = df.duplicated(subset=columns, keep=False)\n    return df[dupes == True]  # noqa: E712\n\n\ndef encode_categorical(df, columns):\n    \"\"\"\n    Encode the specified columns as categorical.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        encode_categorical(df, columns=\"my_categorical_column\")  # one way\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        categorical_cols = ['col1', 'col2', 'col4']\n        jn.DataFrame(df).encode_categorical(columns=categorical_cols)\n\n    :param df: The pandas DataFrame object.\n    :param str/iterable columns: A column name or an iterable (list or tuple)\n        of column names.\n    :returns: A pandas DataFrame\n    \"\"\"\n    if isinstance(columns, list) or isinstance(columns, tuple):\n        for col in columns:\n            assert col in df.columns, \\\n                JanitorError(\"{col} missing from dataframe columns!\".format(col=col))  # noqa: E501\n            df[col] = pd.Categorical(df[col])\n    elif isinstance(columns, str):\n        df[columns] = pd.Categorical(df[columns])\n    else:\n        raise JanitorError('kwarg `columns` must be a string or iterable!')\n    return df\n\n\ndef get_features_targets(df, target_columns, feature_columns=None):\n    \"\"\"\n    Get the features and targets as separate DataFrames/Series.\n\n    The behaviour is as such:\n\n    - `target_columns` is mandatory.\n    - If `feature_columns` is present, then we will respect the column names\n      inside there.\n    - If `feature_columns` is not passed in, then we will assume that the\n      rest of the columns are feature columns, and return them.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        X, y = get_features_targets(df, target_columns=\"measurement\")\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        target_cols = ['output1', 'output2']\n        X, y = jn.DataFrame(df).get_features_targets(target_columns=target_cols)  # noqa: E501\n\n    :param df: The pandas DataFrame object.\n    :param str/iterable target_columns: Either a column name or an iterable\n        (list or tuple) of column names that are the target(s) to be predicted.\n    :param str/iterable feature_columns: (optional) The column name or iterable\n        of column names that are the features (a.k.a. predictors) used to\n        predict the targets.\n    :returns: (X, Y) the feature matrix (X) and the target matrix (Y). Both are\n        pandas DataFrames.\n    \"\"\"\n    Y = df[target_columns]\n\n    if feature_columns:\n        X = df[feature_columns]\n    else:\n        if isinstance(target_columns, str):\n            xcols = [c for c in df.columns if target_columns != c]\n        elif (isinstance(target_columns, list)\n                or isinstance(target_columns, tuple)):\n            xcols = [c for c in df.columns if c not in target_columns]\n        X = df[xcols]\n    return X, Y\n\n\ndef rename_column(df, old, new):\n    \"\"\"\n    Rename a column in place.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = rename_column(\"old_column_name\", \"new_column_name\")\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).rename_column(\"old_column_name\", \"new_column_name\")  # noqa: E501\n\n    This is just syntactic sugar/a convenience function for renaming one column\n    at a time. If you are convinced that there are multiple columns in need of\n    changing, then use the :py:meth:`pandas.DataFrame.rename` method.\n\n    :param str old: The old column name.\n    :param str new: The new column name.\n    :returns: A pandas DataFrame.\n    \"\"\"\n    return df.rename(columns={old: new})\n\n\ndef coalesce(df, columns, new_column_name):\n    \"\"\"\n    Coalesces two or more columns of data in order of column names provided.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = coalesce(df, columns=['col1', 'col2'])\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).coalesce(['col1', 'col2'])\n\n\n    The result of this function is that we take the first non-null value across\n    rows.\n\n    This is more syntactic diabetes! For R users, this should look familiar to\n    `dplyr`'s `coalesce` function; for Python users, the interface\n    should be more intuitive than the :py:meth:`pandas.Series.combine_first`\n    method (which we're just using internally anyways).\n\n    :param df: A pandas DataFrame.\n    :param columns: A list of column names.\n    :param str new_column_name: The new column name after combining.\n    :returns: A pandas DataFrame.\n    \"\"\"\n    series = [df[c] for c in columns]\n\n    def _coalesce(series1, series2):\n        return series1.combine_first(series2)\n    df = df.drop(columns=columns)\n    df[new_column_name] = reduce(_coalesce, series)  # noqa: F821\n    return df\n\n\ndef convert_excel_date(df, column):\n    \"\"\"\n    Convert Excel's serial date format into Python datetime format.\n\n    Implementation is also from `Stack Overflow`.\n\n    .. _Stack Overflow: https://stackoverflow.com/questions/38454403/convert-excel-style-date-with-pandas  # noqa: E501\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = convert_excel_date(df, column='date')\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).convert_excel_date('date')\n\n    :param df: A pandas DataFrame.\n    :param str column: A column name.\n    :returns: A pandas DataFrame with corrected dates.\n    \"\"\"\n    df[column] = (pd.TimedeltaIndex(df[column], unit='d')\n                  + dt.datetime(1899, 12, 30))\n    return df\n\n\ndef fill_empty(df, columns, value):\n    \"\"\"\n    Fill `NaN` values in specified columns with a given value.\n\n    Super sugary syntax that wraps :py:meth:`pandas.DataFrame.fillna`.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = fill_empty(df, columns=['col1', 'col2'], value=0)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).fill_empty(df, columns='col1', value=0)\n\n    :param df: A pandas DataFrame.\n    :param columns: Either a `str` or `list` or `tuple`. If a string is passed\n        in, then only that column will be filled; if a list or tuple of strings\n        are passed in, then they will all be filled with the same value.\n    :param value: The value that replaces the `NaN` values.\n    \"\"\"\n    if isinstance(columns, list) or isinstance(columns, tuple):\n        for col in columns:\n            assert col in df.columns, \\\n                JanitorError(\"{col} missing from dataframe columns!\".format(col=col))  # noqa: E501\n            df[col] = df[col].fillna(value)\n    elif isinstance(columns, str):\n        df[columns] = df[columns].fillna(value)\n    else:\n        raise JanitorError('kwarg `columns` must be a string or iterable!')\n\n    return df\n", "path": "janitor/functions.py"}], "after_files": [{"content": "import datetime as dt\nfrom functools import reduce\n\nimport pandas as pd\n\nfrom .errors import JanitorError\n\nimport re\n\n\ndef _strip_underscores(df, strip_underscores=None):\n    \"\"\"\n    Strip underscores from the beginning, end or both of the\n    of the DataFrames column names.\n\n    .. code-block:: python\n\n        df = _strip_underscores(df, strip_underscores='left')\n\n    :param df: The pandas DataFrame object.\n    :param strip_underscores: (optional) Removes the outer underscores from all\n        column names. Default None keeps outer underscores. Values can be\n        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'\n        and True.\n    :returns: A pandas DataFrame.\n    \"\"\"\n    underscore_options = [None, 'left', 'right', 'both', 'l', 'r', True]\n    if strip_underscores not in underscore_options:\n        raise JanitorError(\n            \"\"\"strip_underscores must be one of: %s\"\"\" % underscore_options\n            )\n\n    if strip_underscores in ['left', 'l']:\n        df = df.rename(columns=lambda x: x.lstrip('_'))\n    elif strip_underscores in ['right', 'r']:\n        df = df.rename(columns=lambda x: x.rstrip('_'))\n    elif strip_underscores == 'both' or strip_underscores is True:\n        df = df.rename(columns=lambda x: x.strip('_'))\n    return df\n\n\ndef clean_names(df, strip_underscores=None):\n    \"\"\"\n    Clean column names.\n\n    Takes all column names, converts them to lowercase, then replaces all\n    spaces with underscores.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = clean_names(df)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).clean_names()\n\n    :param df: The pandas DataFrame object.\n    :param strip_underscores: (optional) Removes the outer underscores from all\n        column names. Default None keeps outer underscores. Values can be\n        either 'left', 'right' or 'both' or the respective shorthand 'l', 'r'\n        and True.\n    :returns: A pandas DataFrame.\n    \"\"\"\n    df = df.rename(\n        columns=lambda x: x.lower()\n                           .replace(' ', '_')\n                           .replace('/', '_')\n                           .replace(':', '_')\n                           .replace(\"'\", '')\n                           .replace('\u2019', '')\n                           .replace(',', '_')\n                           .replace('?', '_')\n                           .replace('-', '_')\n                           .replace('(', '_')\n                           .replace(')', '_')\n                           .replace('.', '_')\n    )\n\n    df = df.rename(columns=lambda x: re.sub('_+', '_', x))\n    df = _strip_underscores(df, strip_underscores)\n    return df\n\n\ndef remove_empty(df):\n    \"\"\"\n    Drop all rows and columns that are completely null.\n\n    Implementation is shamelessly copied from `StackOverflow`_.\n\n    .. _StackOverflow: https://stackoverflow.com/questions/38884538/python-pandas-find-all-rows-where-all-values-are-nan  # noqa: E501\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = remove_empty(df)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).remove_empty()\n\n    :param df: The pandas DataFrame object.\n    :returns: A pandas DataFrame.\n    \"\"\"\n\n    nanrows = df.index[df.isnull().all(axis=1)]\n    df.drop(index=nanrows, inplace=True)\n\n    nancols = df.columns[df.isnull().all(axis=0)]\n    df.drop(columns=nancols, inplace=True)\n\n    return df\n\n\ndef get_dupes(df, columns=None):\n    \"\"\"\n    Returns all duplicate rows.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        get_dupes(df)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        jn.DataFrame(df).get_dupes()\n\n    :param df: The pandas DataFrame object.\n    :param str/iterable columns: (optional) A column name or an iterable (list\n        or tuple) of column names. Following pandas API, this only considers\n        certain columns for identifying duplicates. Defaults to using all\n        columns.\n    :returns: The duplicate rows, as a pandas DataFrame.\n    \"\"\"\n    dupes = df.duplicated(subset=columns, keep=False)\n    return df[dupes == True]  # noqa: E712\n\n\ndef encode_categorical(df, columns):\n    \"\"\"\n    Encode the specified columns as categorical.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        encode_categorical(df, columns=\"my_categorical_column\")  # one way\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        categorical_cols = ['col1', 'col2', 'col4']\n        jn.DataFrame(df).encode_categorical(columns=categorical_cols)\n\n    :param df: The pandas DataFrame object.\n    :param str/iterable columns: A column name or an iterable (list or tuple)\n        of column names.\n    :returns: A pandas DataFrame\n    \"\"\"\n    if isinstance(columns, list) or isinstance(columns, tuple):\n        for col in columns:\n            assert col in df.columns, \\\n                JanitorError(\"{col} missing from dataframe columns!\".format(col=col))  # noqa: E501\n            df[col] = pd.Categorical(df[col])\n    elif isinstance(columns, str):\n        df[columns] = pd.Categorical(df[columns])\n    else:\n        raise JanitorError('kwarg `columns` must be a string or iterable!')\n    return df\n\n\ndef get_features_targets(df, target_columns, feature_columns=None):\n    \"\"\"\n    Get the features and targets as separate DataFrames/Series.\n\n    The behaviour is as such:\n\n    - `target_columns` is mandatory.\n    - If `feature_columns` is present, then we will respect the column names\n      inside there.\n    - If `feature_columns` is not passed in, then we will assume that the\n      rest of the columns are feature columns, and return them.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        X, y = get_features_targets(df, target_columns=\"measurement\")\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        target_cols = ['output1', 'output2']\n        X, y = jn.DataFrame(df).get_features_targets(target_columns=target_cols)  # noqa: E501\n\n    :param df: The pandas DataFrame object.\n    :param str/iterable target_columns: Either a column name or an iterable\n        (list or tuple) of column names that are the target(s) to be predicted.\n    :param str/iterable feature_columns: (optional) The column name or iterable\n        of column names that are the features (a.k.a. predictors) used to\n        predict the targets.\n    :returns: (X, Y) the feature matrix (X) and the target matrix (Y). Both are\n        pandas DataFrames.\n    \"\"\"\n    Y = df[target_columns]\n\n    if feature_columns:\n        X = df[feature_columns]\n    else:\n        if isinstance(target_columns, str):\n            xcols = [c for c in df.columns if target_columns != c]\n        elif (isinstance(target_columns, list)\n              or isinstance(target_columns, tuple)):\n            xcols = [c for c in df.columns if c not in target_columns]\n        X = df[xcols]\n    return X, Y\n\n\ndef rename_column(df, old, new):\n    \"\"\"\n    Rename a column in place.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = rename_column(\"old_column_name\", \"new_column_name\")\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).rename_column(\"old_column_name\", \"new_column_name\")  # noqa: E501\n\n    This is just syntactic sugar/a convenience function for renaming one column\n    at a time. If you are convinced that there are multiple columns in need of\n    changing, then use the :py:meth:`pandas.DataFrame.rename` method.\n\n    :param str old: The old column name.\n    :param str new: The new column name.\n    :returns: A pandas DataFrame.\n    \"\"\"\n    return df.rename(columns={old: new})\n\n\ndef coalesce(df, columns, new_column_name):\n    \"\"\"\n    Coalesces two or more columns of data in order of column names provided.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = coalesce(df, columns=['col1', 'col2'])\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).coalesce(['col1', 'col2'])\n\n\n    The result of this function is that we take the first non-null value across\n    rows.\n\n    This is more syntactic diabetes! For R users, this should look familiar to\n    `dplyr`'s `coalesce` function; for Python users, the interface\n    should be more intuitive than the :py:meth:`pandas.Series.combine_first`\n    method (which we're just using internally anyways).\n\n    :param df: A pandas DataFrame.\n    :param columns: A list of column names.\n    :param str new_column_name: The new column name after combining.\n    :returns: A pandas DataFrame.\n    \"\"\"\n    series = [df[c] for c in columns]\n\n    def _coalesce(series1, series2):\n        return series1.combine_first(series2)\n    df = df.drop(columns=columns)\n    df[new_column_name] = reduce(_coalesce, series)  # noqa: F821\n    return df\n\n\ndef convert_excel_date(df, column):\n    \"\"\"\n    Convert Excel's serial date format into Python datetime format.\n\n    Implementation is also from `Stack Overflow`.\n\n    .. _Stack Overflow: https://stackoverflow.com/questions/38454403/convert-excel-style-date-with-pandas  # noqa: E501\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = convert_excel_date(df, column='date')\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).convert_excel_date('date')\n\n    :param df: A pandas DataFrame.\n    :param str column: A column name.\n    :returns: A pandas DataFrame with corrected dates.\n    \"\"\"\n    df[column] = (pd.TimedeltaIndex(df[column], unit='d')\n                  + dt.datetime(1899, 12, 30))\n    return df\n\n\ndef fill_empty(df, columns, value):\n    \"\"\"\n    Fill `NaN` values in specified columns with a given value.\n\n    Super sugary syntax that wraps :py:meth:`pandas.DataFrame.fillna`.\n\n    Functional usage example:\n\n    .. code-block:: python\n\n        df = fill_empty(df, columns=['col1', 'col2'], value=0)\n\n    Method chaining example:\n\n    .. code-block:: python\n\n        df = pd.DataFrame(...)\n        df = jn.DataFrame(df).fill_empty(df, columns='col1', value=0)\n\n    :param df: A pandas DataFrame.\n    :param columns: Either a `str` or `list` or `tuple`. If a string is passed\n        in, then only that column will be filled; if a list or tuple of strings\n        are passed in, then they will all be filled with the same value.\n    :param value: The value that replaces the `NaN` values.\n    \"\"\"\n    if isinstance(columns, list) or isinstance(columns, tuple):\n        for col in columns:\n            assert col in df.columns, \\\n                JanitorError(\"{col} missing from dataframe columns!\".format(col=col))  # noqa: E501\n            df[col] = df[col].fillna(value)\n    elif isinstance(columns, str):\n        df[columns] = df[columns].fillna(value)\n    else:\n        raise JanitorError('kwarg `columns` must be a string or iterable!')\n\n    return df\n", "path": "janitor/functions.py"}]}