We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
clean_names breaks for MultiIndex Columns
MultiIndex columns returns `expected str not tuple` type error.

If you instead used,
`df.rename(columns=lambda x: x.lower().replace('_', ''))`
this would work for standard and MultiIndex DataFrames.

Can do PR if required.

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

{"golden_diff": "diff --git a/janitor/functions.py b/janitor/functions.py\n--- a/janitor/functions.py\n+++ b/janitor/functions.py\n@@ -31,26 +31,22 @@\n     :param df: The pandas DataFrame object.\n     :returns: A pandas DataFrame.\n     \"\"\"\n-    columns = [(c.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-                ) for c in df.columns]\n-\n-    newcolumns = []\n-    for col in columns:\n-        # NOTE: Replace repeating underscores with single ones\n-        newcol = re.sub('[_]{2,}', '_', col)\n-        newcolumns.append(newcol)\n-    df.columns = newcolumns\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", "issue": "clean_names breaks for MultiIndex Columns\nMultiIndex columns returns `expected str not tuple` type error.\r\n\r\nIf you instead used,\r\n`df.rename(columns=lambda x: x.lower().replace('_', ''))`\r\nthis would work for standard and MultiIndex DataFrames.\r\n\r\nCan do PR if required.\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    columns = [(c.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                ) for c in df.columns]\n\n    newcolumns = []\n    for col in columns:\n        # NOTE: Replace repeating underscores with single ones\n        newcol = re.sub('[_]{2,}', '_', col)\n        newcolumns.append(newcol)\n    df.columns = newcolumns\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 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"}]}