Repository: pyjanitor-devs/pyjanitor

Issue:
Select_Columns Function Added Suggestion...
I see there is remove columns function.  I think a select_columns function would work nice.  It would be cleaner and easier to understand then df[['col1',col2','col3']].


Discussion:
@jcvall if you were to design the API for this function, what would it be? What would be the relevant arguments, and what would it return? What corner cases might need to be taken care of?

I'm asking because I'd like to solicit API designs that will eventually get used, rather than design what I think is useful only to find it doesn't get used. Also, some initially simple-looking functions (e.g. clean_names) turned out to have a bunch of possible additional kwargs that were useful that I didn't think of at first glance!

This is your opportunity to shape how the API looks like too!
At first glance, now that I have had some time to think about it, an API that might be useful would look like this:

```python
df = (
    pd.DataFrame(...)
    .select_columns(col_names=[col1, col_2, col_3, ...])
)
```

One could have an optional `invert=True`, such that we select the complement of the passed-in column names:

```python
df = (
    pd.DataFrame(...)
    .select_columns(col_names=[col1, col_2, col_3, ...], invert=True)
)
```

@jcvall any thoughts? The only thing I would think needs a second-pass look would be whether `invert` is the best kwarg name or not.
I have been coding for a few years in python and r, but I apologize because I am new to this (Github). 
 This exactly what I had in mind!!!!  I think it will be well used.  One of the first things I do is select my columns when I start a line of code.  I use df[['col1','col2',..]] in python and %>% select() in r all the time.  This would probably be used in conjunction with most functions in this package.  I was giving it a stab (with the select_columns function) last night, but I am honestly not at this level yet to edit code in a package (I hope to be one day soon).  What I would love to do is create a cheat sheet for pyjanitor (like we see in rstudio).  Your package has a lot going on and I really find myself just importing pandas, numpy, and janitor and I am off and running.  It has so much good stuff here though I need a cheat sheet.  By the way...invert is great (also...unselect, deselect, remove could work. But remove would be not my first choice).  I would like to help more, so I will be looking for ways to make a contribution.  
@jcvall, if you're willing to do so, I'd like to invite you to paste your implementation here, where I can guide you through the programming model needed to make `select_columns` a thing. 

I can see how the code should work, but I'm happy to give the opportunity to newcomers. Everybody had a start somewhere, including myself, and in my case, it was with the matplotlib devs, doing some very simple and repetitive things.

Let me know if you'd like to give it a shot - it will be a rewarding process! If not, no worries, I can see how the code would work, and can put up a first pass on `select_columns` in the near future.
I’m in.  I will give it a go. 

<sub>Sent with <a href="http://githawk.com">GitHawk</a></sub>
Here we go....a little trial and error and this is what I have so far.

```python
def select_columns(df,col, invert = False ):
    
    if invert == False:
        df = df[col]
        return df

    elif invert == True:
        df.drop(columns=col)
        return df
```
@jcvall looks great! I'd do a few modifications, with explanations below:

```python
from typing import List
import pandas as pd

#[0]
def select_columns(df: pd.DataFrame, columns: List, invert: bool = False):
    """ #[1]
    Method-chainable selection of columns.

    Optional ability to invert selection of columns available as well.

    Method-chaining example:

    ..code-block:: python
        
        df = pd.DataFrame(...).select_columns(['a', 'b', 'c'], invert=True)

    :param df: A pandas DataFrame.
    :param columns: A list of columns to select.
    :param invert: Whether or not to invert the selection. This will result in selection of
        the complement of the columns provided.
    :returns: A pandas DataFrame with the columns selected.
    """
    #[2]
    if invert:
        #[3]
        return df.drop(columns=columns)

    else:
        return df[columns]
```

Note 0: Used the keyword argument `columns`, to preserve consistency with the rest of the API. Currently `coalesce`, `concatenate_columns`, `encode_categorical`, `fill_empty`, `get_dupes` and `label_encode` all use the `columns` kwarg. Consistency and repetition helps with learning the API better.

Note 1: I added a docstring. Docstrings helps us document the intent behind the function and the parameters, and get automatically built as part of the docs. (It's pretty amazing what readthedocs does, be sure to support the project!)

Note 2: It's usually pythonic to write boolean conditionals as:

```python
if X:
    # do something here
else:
    # do something else here
```

Note 3: This is inspired by a bit of technical detail, but if I remember correctly, `df.drop(columns=[columns])` results in data being copied over. I'm ok with that for now, but we may in the future end up with a PR from a certain colleague of mine (@zbarry, ahem ahem) who might propose a modification that doesn't result in the dataframe being copied. This is related to issue #76 and #79, for which I think we will end up needing a sane default being set sometime in the future.

Are you familiar with Git and software development workflows? If not, ping back here, I'm happy to guide you through it. We can also do a Google Hangouts call if you'd like. I'd like your contribution to be recorded in the version control history of the repository.
🤔
Wow.  Awesome.  I don’t know git that well.  Would be honored to have you walk me through it.  A google chat is cool with me.  My schedule is crazy with the kids, but might have a window on the weekends.  If that doesn’t work you can guide me here. Again thanks.

<sub>Sent with <a href="http://githawk.com">GitHawk</a></sub>
@jcvall totally happy to do so here. That way we can avoid trying to coordinate times. Asynchronous work is always good, even better when there's no pressures involved.

Pardon me if the instructions below cover what you already know, I'm working off the assumption that your git level is at a software carpentry student's level. (i.e. "may have typed some git commands, but don't really have the mental model.)

First off, fork the repository. 
<img width="1009" alt="screen shot 2018-12-10 at 8 35 16 am" src="https://user-images.githubusercontent.com/2631566/49735963-bb073980-fc56-11e8-8da9-adae4e1e27cf.png">

This will give you a copy of the repository that lives under your username on GitHub. It doesn't automatically sync with my copy, as you'll need to execute some commands to make that happen. 

Secondly, clone the repository locally. There are two options: you can do it by SSH, or you can do it by HTTPS. Both give the exact same result, but SSH may be simpler once you've set it up. By convention, under my home folder on my Mac, I have a folder in which I place all of my projects that reside on GitHub. Cloning a repository simply results in a folder that lives on your local hard drive, and that one is a copy of the fork that you have. I would suggest that you use the GitHub Desktop application, if you're not familiar with Git commands at the command line. It'll give you an easier time ramping up. (Learning the git commands, though, is extremely useful, particularly for automation purposes.)

<img width="551" alt="screen shot 2018-12-10 at 8 42 19 am" src="https://user-images.githubusercontent.com/2631566/49736281-b000d900-fc57-11e8-9b27-4859e3107084.png">

The cloned repository will have the exact history of your forked repository, which will have the exact history of my repository (which we canonically would consider "upstream" to your forked repository, that which is the "origin" for your cloned repository), at the time that you forked it.

What you'll want to do, now is create a branch on your local copy. GitHub Desktop doesn't show you all of the files in the repository, only the ones that are changed. On the GitHub desktop interface, you can click on the branches drop-down menu, and then click on "create branch". This branch will house changes to the source code, without affecting the "master" branch that you have.

<img width="672" alt="screen shot 2018-12-10 at 8 47 37 am" src="https://user-images.githubusercontent.com/2631566/49736542-79778e00-fc58-11e8-8e53-3c002f58b714.png">

Give it an informative name, e.g. `select-columns`, and then start making changes to the source code, in this case, by copy/pasting my modified version of your function into the `functions.py` file.

When you're done with those steps, ping back here, happy to guide you through the rest of the steps involved.
@jcvall, this is an awesome suggestion! I was an R coder for many years and used `dplyr::select` often. 

What's great about the R version of `select` is that it has other ways of selecting columns, such as `starts_with` or `ends_with` which is really useful when trying to slice down a `data.frame`.

And you can join those as well. For example, you can pass `['col1', ends_with('name')]` to match `col1` and any column that ends with `name`.

I'd be willing to add those features if you'd like.
Sure.   I like to learn how to do this so I am ok with you using what Eric did above and just adding to it. Can you copy the code here, then I will give it my best shot to upload it to github.  Very excited!  I have some other possible ideas and would like jump in to help when I can. Like Eric said, you got to start somewhere. 

<sub>Sent with <a href="http://githawk.com">GitHawk</a></sub>
@jcvall thanks for the enthusiasm! However, let's go slowly with this. It's okay to have a feature-limited first pass on the PR, and I'd actually prefer it, because it makes reviewing the code much easier. :smile: 
Eric, sounds like a plan.  I saved the function on my computer under my github desktop folder.  Ready for further instructions. 👍

<sub>Sent with <a href="http://githawk.com">GitHawk</a></sub>
Ok! Now, what you'll want to do is to "push" those changes to your fork of `pyjanitor` that resides on GitHub. To do so: 

1. In the GitHub desktop interface, publish your branch. (There should be a button at the top.)
<img width="523" alt="screen shot 2018-12-14 at 7 59 32 am" src="https://user-images.githubusercontent.com/2631566/50005024-e059b780-ff76-11e8-8e64-ae055f626cc0.png">

2. GH desktop will also show you the files that have changed. You'll want to enter a commit message.
![screen shot 2018-12-14 at 8 04 15 am](https://user-images.githubusercontent.com/2631566/50005078-0e3efc00-ff77-11e8-8965-2f43ba448b5b.png)

This commit message tells me a summary of the changes that you have made thus far. 

3. Hit the blue commit button. Then, you'll see that you can "Push Origin" in the top toolbar. Click that. It will take your changes that are local to your computer, and send it up to your fork of pyjanitor that lives on GitHub.

<img width="521" alt="screen shot 2018-12-14 at 8 06 34 am" src="https://user-images.githubusercontent.com/2631566/50005151-552cf180-ff77-11e8-84ef-2dbabbcf2443.png">

4. Now go to the main GitHub pyjanitor page. You should see something similar to this:
<img width="1019" alt="screen shot 2018-12-14 at 8 08 29 am" src="https://user-images.githubusercontent.com/2631566/50005206-8c030780-ff77-11e8-882e-9343d4a6f99f.png">

You'll want to now make a "pull request" - you're requesting me to pull in changes from your fork into my branch. 

<img width="840" alt="screen shot 2018-12-14 at 8 09 42 am" src="https://user-images.githubusercontent.com/2631566/50005292-e308dc80-ff77-11e8-933e-3ccf2f758af3.png">

Fill out the two text boxes. The top one contains a summary of changes, and the bottom one contains a checklist for you to follow. Follow the instructions carefully there, and then submit your PR!

Resolving patch:
diff --git a/janitor/functions.py b/janitor/functions.py
--- a/janitor/functions.py
+++ b/janitor/functions.py
@@ -546,16 +546,12 @@ def fill_empty(df, columns, value):
         for col in columns:
             assert (
                 col in df.columns
-            ), "{col} missing from dataframe columns!".format(
-                col=col
-            )
+            ), "{col} missing from dataframe columns!".format(col=col)
             df[col] = df[col].fillna(value)
     else:
         assert (
             columns in df.columns
-        ), "{col} missing from dataframe columns!".format(
-            col=columns
-        )
+        ), "{col} missing from dataframe columns!".format(col=columns)
         df[columns] = df[columns].fillna(value)
 
     return df
@@ -1659,3 +1655,31 @@ def check(varname: str, value, expected_types: list):
                 varname=varname, expected_types=expected_types
             )
         )
+
+
+@pf.register_dataframe_method
+def select_columns(df: pd.DataFrame, columns: List, invert: bool = False):
+    """
+    Method-chainable selection of columns.
+
+    Optional ability to invert selection of columns available as well.
+
+    Method-chaining example:
+
+    ..code-block:: python
+
+        df = pd.DataFrame(...).select_columns(['a', 'b', 'c'], invert=True)
+
+    :param df: A pandas DataFrame.
+    :param columns: A list of columns to select.
+    :param invert: Whether or not to invert the selection.
+        This will result in selection ofthe complement of the columns provided.
+    :returns: A pandas DataFrame with the columns selected.
+    """
+
+    if invert:
+
+        return df.drop(columns=columns)
+
+    else:
+        return df[columns]


Test patch:
diff --git a/tests/test_functions.py b/tests/test_functions.py
--- a/tests/test_functions.py
+++ b/tests/test_functions.py
@@ -872,3 +872,17 @@ def test_reset_index_inplace_drop(dataframe):
         dataframe.reset_index(drop=True),
         dataframe.reset_index_inplace(drop=True),
     )
+
+
+def test_select_columns(dataframe):
+    columns = ["a", "Bell__Chart", "cities"]
+    df = dataframe.select_columns(columns=columns)
+
+    pd.testing.assert_frame_equal(df, dataframe[columns])
+
+
+def test_select_columns_invert(dataframe):
+    columns = ["a", "Bell__Chart", "cities"]
+    df = dataframe.select_columns(columns=columns, invert=True)
+    columns2 = ["decorated-elephant", "animals@#$%^"]
+    pd.testing.assert_frame_equal(df, dataframe[columns2])