diff --git a/.gitignore b/.gitignore index 69b0837..ece8b0e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,11 @@ bin/miniconda3.sh models/*.joblib !models/keep **/.ipynb_checkpoints +.noseids -# ignoring all json files in project root folder to prevent accidental commits of secrets -/*.json +# test coverage +htmlcov +.coverage # IDE config .vscode/ @@ -18,8 +20,5 @@ models/*.joblib # linters .mypy_cache -# personal notes -docs/todos.md - # OSX .DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7760ca2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.6-slim + +RUN apt-get update + +WORKDIR /code +COPY . /code + +RUN pip install -U pip && pip install -r requirements.txt diff --git a/README.md b/README.md index 2d746ca..15e47c7 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,47 @@ # clean-code-ml +Now available as a free tutorial series: https://bit.ly/2yGDyqT šŸ˜Ž + ## Table of Contents + - [Introduction](#introduction) +- [Dev Productivity](docs/dev-tools.md) + - Use Docker and stop hearing "Works on my machine!" + - Ensure reproducibility + - VS Code productivity tips (or "Know Your IDE") - [Variables](docs/variables.md) - - Variable names should reveal intent - - Use meaningful and pronounceable variable names - - Use the same vocabulary for the same type of variable - - Avoid magic numbers and magic strings - - Use variables to keep code "DRY" ("Don't Repeat Yourself") - - Use explanatory variables - - Avoid mental mapping - - Don't add unneeded context + - Variable names should reveal intent + - Use meaningful and pronounceable variable names + - Use the same vocabulary for the same type of variable + - Avoid magic numbers and magic strings + - Use variables to keep code "DRY" ("Don't Repeat Yourself") + - Use explanatory variables + - Avoid mental mapping + - Don't add unneeded context - [Dispensables](docs/dispensables.md) - - Avoid comments - - Remove dead code - - Avoid print statements (even glorified print statements such as df.head(), df.describe(), df.plot()) + - Avoid comments + - Remove dead code + - Avoid print statements (even glorified print statements such as df.head(), df.describe(), df.plot()) - [Functions](docs/functions.md) - - Use functions to keep code "DRY" - - Functions should do one thing - - Functions should only be one level of abstraction - - Function names should say what they do - - Use type hints to improve readability - - Avoid side effects - - Avoid unexpected side effects on values passed as function parameters - - Function arguments (2 or fewer ideally) - - Use default arguments instead of short circuiting or conditionals - - Don't use flags as function parameters + - Use functions to keep code "DRY" + - Functions should do one thing + - Functions should only be one level of abstraction + - Function names should say what they do + - Use type hints to improve readability + - Avoid side effects + - Avoid unexpected side effects on values passed as function parameters + - Function arguments (2 or fewer ideally) + - Use default arguments instead of short circuiting or conditionals + - Don't use flags as function parameters - [Design](docs/design.md) - - Avoid exposing your internals (Keep implementation details hidden) + - Avoid exposing your internals (Keep implementation details hidden) ## Introduction Clean code practices (from [Clean Code](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) and [Refactoring](https://www.amazon.com/Refactoring-Improving-Existing-Addison-Wesley-Signature/dp/0134757599)) adapted for machine learning / data science workflows in Python. This is not a style guide. It's a guide to producing readable, reusable, and refactorable software. -If you’ve tried your hand at machine learning or data science, you would know that code can get messy, quickly. +If you’ve tried your hand at machine learning or data science, you would know that code can get messy, quickly. Unclean code adds to complexity by making code difficult to read and modify. As a consequence, changing code to respond to business needs becomes increasingly difficult, and sometimes even impossible. This has been written about extensively in several languages, and even in Python (e.g. Clean Code, Refactoring, clean-code-python). In this repo, we have adapted these principles for data science / machine learning codebases. @@ -42,6 +49,18 @@ Targets Python3.7+ Inspired by [clean-code-javascript](https://github.com/ryanmcdermott/clean-code-javascript) and forked from [clean-code-python](https://github.com/zedr/clean-code-python). +## The 5 S's of Clean Code + +By James O Coplien (Source: Foreword of Clean Code (Robert C. Martin)) + +In about 1951, a quality approach called Total Productive Maintenance (TPM) came on the Japanese scene. Its focus is on maintenance rather than on production. One of the major pillars of TPM is the set of so-called 5S principles: +- **Seiri**, or organization (think **sort** in English). Knowing where things are—using approaches such as suitable naming—is crucial. +- **Seiton**, or tidiness (think **systematize** in English). There is an old American saying: A place for everything, and everything in its place. A piece of code should be where you expect to find it—and, if not, you should re-factor to get it there. +- **Seiso**, or cleaning (think **shine** in English): Keep the workplace free of hanging wires, grease, scraps, and waste. What do the authors here say about littering your code with comments and commented-out code lines that capture history or wishes for the future? Get rid of them. +- **Seiketsu**, or **standardization**: The group agrees about how to keep the workplace clean. Have a consistent coding style and set of practices within the group. +- **Shutsuke**, or discipline (**self-discipline**). This means having the discipline to follow the practices and to frequently reflect on one’s work and be willing to change. + ## Hands-on Exercise If you'd like to try out these practices, we've created a [refactoring exercise](./docs/refactoring-exercise.md) which you can follow along. Starting with [a jupyter notebook with many code smells](notebooks/titanic-notebook-1.ipynb), you can apply these clean code principles and refactor it to be readable and maintainable. The sample final solution can be found in [`src/train.py`](src/train.py). + diff --git a/bin/install_deps_locally.sh b/bin/install_deps_locally.sh new file mode 100755 index 0000000..37b9a6f --- /dev/null +++ b/bin/install_deps_locally.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +python3 -m venv .venv + +source .venv/bin/activate +pip install -r requirements.txt \ No newline at end of file diff --git a/bin/setup.sh b/bin/setup.sh index 6cad928..410135a 100755 --- a/bin/setup.sh +++ b/bin/setup.sh @@ -4,10 +4,10 @@ set -e virtual_env_name="$(basename $(pwd))" -if [[ $OSTYPE != "darwin"* ]]; then +if [[ $OSTYPE == "msys" ]]; then echo "[INFO] Non-Mac OSX operating system detected" - echo "[TODO] Open http://continuum.io/downloads with your web browser" - echo "[TODO] Download the Python 3 installer for your OS" + echo "[TODO] Open https://docs.conda.io/en/latest/miniconda.html with your web browser" + echo "[TODO] Download the Miniconda 3 installer for your OS" echo "[TODO] Run the installer. Go with the defaults, except make sure to check 'Make Anaconda the default Python'" echo "[INFO] Exiting..." exit 0 @@ -48,4 +48,4 @@ fi echo "[INFO] Done!" echo "[INFO] To activate the virtual environment, run: source activate ${virtual_env_name}" echo "[INFO] If you see a 'command not found: conda' error message, restart your shell/terminal" -echo "[INFO] To deactivate the virtual environment, run: source deactivate" \ No newline at end of file +echo "[INFO] To deactivate the virtual environment, run: conda deactivate" \ No newline at end of file diff --git a/docs/CI.md b/docs/CI.md index 74ab8e8..b558024 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -2,15 +2,16 @@ When we have tests, we can take it to the next level and implement another agile practice: continuous integration (CI). -### What is continuous integration? +## What is continuous integration Continuous integration (of code) is a development practice that requires developers to integrate code into a shared repository several times a day. Each check-in is then verified by an automated build, allowing teams to detect problems early. By integrating regularly, you can detect errors quickly, and locate them more easily. -### How do we do it? +## How do we do it There are several CI tools that you can use (e.g. [CircleCI](https://circleci.com/), [GoCD](https://www.gocd.org/), [TravisCI](https://travis-ci.org/), etc). For this workshop, we will use CircleCI. -### Steps for setting up your CI pipeline: +## Steps for setting up your CI pipeline + - Create CircleCI account: https://circleci.com/ (free) - Create circleci project. Visit https://circleci.com/dashboard, login and click on 'Add Projects' on the left panel. Click on 'Set up project' for the git repo of your choice - Add [.circleci/config.yml](../.circleci/config-reference.yml) to your repo. Commit and push your code to GitHub diff --git a/docs/FAQs.md b/docs/FAQs.md new file mode 100644 index 0000000..cdf015d --- /dev/null +++ b/docs/FAQs.md @@ -0,0 +1,60 @@ +# FAQs + +### IDE configuration +To get the optimal coding workflow, we often rely on intellisense and code completion provided by our code editors. Unfortunately, this becomes [hard](https://github.com/Microsoft/vscode-python/issues/79#issuecomment-348193800) when our python virtual environment is contained within the docker container. As a workaround, you can: +- Run `bin/configure_venv_locally.sh`. This will create a duplicate python virtual environment (by the name of `.venv-local`) on your host (i.e. your computer) +- Configure your IDE with the python path of this virtual environment: + - [VS Code](https://code.visualstudio.com/docs/python/environments#_select-and-activate-an-environment) + - [PyCharm (community edition)](https://www.jetbrains.com/help/pycharm/creating-virtual-environment.html) + - PyCharm (professional edition) users: you don't need this workaround. You can follow set up your IDE to use the virtual environment in the Docker container (see [instructions])(https://www.jetbrains.com/help/pycharm/using-docker-as-a-remote-interpreter.html) +- configure autosave + + +### Common errors and how to fix them + +1. `docker run` causes the following error: +```shell +docker: Error response from daemon: driver failed programming external connectivity on endpoint elated_brown (a26aea6b1fcd5f286dd7164b42 +47de2f958f8280140b51ec39eed13e3801037b): Bind for 0.0.0.0:8080 failed: port is already allocated. + +# Reason: some container is already running and taken port 8080 +# Solution: +# 1. get id of running container +docker ps + +# 2 stop container +docker stop +# e.g. docker stop 9d57a1f8f49a + +# Now you can run `docker run` again +``` + +### [Windows users] Common errors and how to fix them + +1. If you encounter the following error, when running `docker run ... -p 8080:8080 ...`: +```shell +docker: Error response from daemon: driver failed programming external connectivity on endpoint zealous_rubin (f70ddf46807daed2b1a24e3f897af1dd587b97b30ef676c8fcdba40598756 +c49): Error starting userland proxy: mkdir /port/tcp:0.0.0.0:8080:tcp:172.17.0.2:8080: input/output error. + +# Solution: +# 1. Right click docker icon --> Settings --> Daemon --> Ensure 'Experimental Features' is unchecked +# 2. Restart docker +``` + +2. You mounted a volume (e.g. `docker run -v /$(pwd):/home/`) but you don't see the mounted directory: +```shell +# solution: replace /$(pwd) with the full path to the directory that you wish to mount: +winpty docker run -it -v C:\\Users\\path\\to\\your\\ci-workshop-app:/home/ci-workshop-app -p 8080:8080 ci-workshop-app bash + +# Note: to find the full path, you can run `pwd` in the directory that you wish to mount, and manually replace forward slashes (/) with double backslashes (\\) +``` +This is an open issue in Docker for Windows that has to do with how Git Bash converts filepaths: https://github.com/docker/toolbox/issues/673 + +3. You edited a shell script and tried to run it but got some error about invalid characters (^M) +```shell +# on git bash, convert line endings to unix endings +dos2unix bin/my_file.sh + +# now you can execute your script +bin/my_file.sh +``` diff --git a/docs/cheat-sheet.md b/docs/cheat-sheet.md index 2a4da98..9a49a77 100644 --- a/docs/cheat-sheet.md +++ b/docs/cheat-sheet.md @@ -3,18 +3,22 @@ ## Table of Contents [Variables](#variables) + - Variable names should reveal intent [Dispensables](#dispensables) + - Avoid comments - Remove dead code - Avoid print statements (even glorified print statements such as df.head(), df.describe(), df.plot()) [Functions](#functions) + - Use functions to keep code "DRY" - Functions should do one thing [Design](#design) + - Don't expose your internals (Keep implementation details hidden) --- @@ -22,9 +26,10 @@ ## Variables ### Variable names should reveal intent -We will read more code than we will ever write. It's important for our code to express intent so that our readers don't have to waste mental effort to figure out puzzles. -One common culprit in data science code is dataframes. Every dataframe is named as `df`. In software programming, it's an unusual (and bad) practice to embed information about variable types in the variable name (e.g. we would probably never write `string = 'Hello friends'`. Instead, we would write `greeting = 'Hello friends'`). +We will read more code than we will ever write. It's important for our code to express intent so that our readers don't have to waste mental effort to figure out puzzles. + +One common culprit in data science code is dataframes. Every dataframe is named as `df`. In software programming, it's an unusual (and bad) practice to embed information about variable types in the variable name (e.g. we would probably never write `string = 'Hello friends'`. Instead, we would write `greeting = 'Hello friends'`). **Bad:** @@ -61,6 +66,7 @@ total_loan_amount = monthly_loans_in_december.sum() ### Avoid comments Comments can become problematic in a few ways: + - If some code needs comments, it's a smell for deeper issues (e.g. bad variable naming, violation of single responsibility principle, poor abstraction) - Comments can grow stale and they can lie - Comments can make code even harder to understand when there's too much of it. @@ -86,7 +92,7 @@ if employee.isEligibleForBenefits(): ### Remove dead code -Dead code is code which is executed but whose result is never used in any other computation. Dead code is yet another unrelated thing that developers have to hold in our head when coding. It adds unnecessary cognitive load. +Dead code is code which is executed but whose result is never used in any other computation. Dead code is yet another unrelated thing that developers have to hold in our head when coding. It adds unnecessary cognitive load. If there's code which does not change the result of the program whether it runs or not, then it's not required for the code to run. We should remove it to keep the codebase clean. When we make small and frequent commits, we need not fear losing code. If we ever need those lines of code again, we can easily recover it from the git history. @@ -94,7 +100,6 @@ One common type of dead code are print statements (even glorified print statemen When our codebase has unit tests, the feedback from unit tests replaces the manual/visual feedback we used to use (e.g. df.head()) to check if things are working. This allows us to remove these print statements and make our code more readable. - **Bad:** ```python @@ -160,10 +165,10 @@ print(gaussian_accuracy) def train_model(ModelClass, X_train, Y_train, **kwargs): model = ModelClass(**kwargs) model.fit(X_train, Y_train) - + accuracy_score = round(model.score(X_train, Y_train) * 100, 2) print(f'accuracy ({ModelClass.__name__}): {accuracy_score}') - + return model, accuracy_score decision_tree_model, decision_tree_accuracy = train_model(DecisionTreeClassifier, X_train, Y_train) @@ -176,6 +181,7 @@ gaussian_model , gaussian_accuracy = train_model(GaussianNB, X_train, Y --- ### Functions should do one thing + This is by far the most important rule in software engineering. When functions do more than one thing, they are harder to compose, test, and reason about. When you can isolate a function to just one action, they can be refactored easily and your code will read much cleaner. If you take nothing else away from this guide other than this, you'll be ahead of many developers. **[⬆ back to top](#table-of-contents)** @@ -213,4 +219,4 @@ df['FareBand'] = df['Fare'] df['FareBand'] = categorize_column(df['Fare'], num_bins=4) ``` -**[⬆ back to top](#table-of-contents)** \ No newline at end of file +**[⬆ back to top](#table-of-contents)** diff --git a/docs/design.md b/docs/design.md index c8c26b6..580f770 100644 --- a/docs/design.md +++ b/docs/design.md @@ -1,6 +1,12 @@ -## **Design** +# **Design** -### Avoid exposing your internals (Keep implementation details hidden) +## Avoid exposing your internals (Keep implementation details hidden) + +```txt +"The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise." + +- Edsger Dijkstra +``` Functions and classes simplify our code by abstracting away complicated implementation details and replacing them with a simpler representation - its name. When implementation details are all laid bare in a Jupyter notebook without any abstractions (functions), we are forced to understand the **how**'s in order to find out **what**'s happening. diff --git a/docs/dev-tools.md b/docs/dev-tools.md new file mode 100644 index 0000000..03a7e3b --- /dev/null +++ b/docs/dev-tools.md @@ -0,0 +1,42 @@ +# Dev Productivity + +## Use Docker and stop hearing "Works on my machine!" + +TL;DR: [Get started with Docker in 17 minutes](https://www.youtube.com/watch?v=Vun1DL9zGuM&list=PLO9pkowc_99ZhP2yuPU8WCfFNYEx2IkwR&index=2) + +Actual story: + +- Me: wow, cool repo! so impress, much fancy. +- Me: clone repo +- Me: run repo +- Computer: `MissingRequirementError: Can't import 'OpenSSL' which is part of 'pyopenssl>=0.14'` +- Me (2 hours later): stackoverflow + 30 open tabs +- Me (2 days later): uninstalled python2 and python3 on my computer (true story) +- Me (3 days later): f\*\*\* this repo + +dependency hell (source: xkcd) + +The same can happen at work. After a few days/weeks of building something and installing various project-level and OS-level dependencies to get things to work, it can be extremely painful (if not impossible) to recall the steps that someone (maybe your colleague / your manager) needs to run to see your awesome work. If you've been in this situation before, you know that this is a waste of time. + +It's much better to reduce entropy and start any project right by ensuring that whatever works on your machine will work on another machine (e.g. your colleague's laptop or the machines that you're deploying your software onto). **With Docker, you can ensure that in a few steps.** + +Docker allows us to manage the following dependencies in a single place: + +- OS dependencies (e.g. `gcc`, `curl`) +- Python runtime version (e.g. Python 3.8) +- CLI tools dependencies +- Project-level dependencies (e.g. `pandas`, `numpy`) + +Sample template project: https://github.com/davified/docker-python-template + +## VS Code productivity tips (or "Know Your IDE") + +- [Configuring IntelliSense and autocomplete in VS Code](https://www.youtube.com/watch?v=KUvqDINDzFE&list=PLO9pkowc_99ZhP2yuPU8WCfFNYEx2IkwR&index=6) +- [How to set up auto-formatting](https://www.youtube.com/watch?v=uO7Zfa_65t8&list=PLO9pkowc_99ZhP2yuPU8WCfFNYEx2IkwR&index=7) +- [How to reduce errors and code smells with a linter](https://www.youtube.com/watch?v=CVUryn8szcw&list=PLO9pkowc_99ZhP2yuPU8WCfFNYEx2IkwR&index=8) + +(Work in progress - Tutorial in written form) + +## Ensure reproducibility + +(Work in progress) diff --git a/docs/dispensables.md b/docs/dispensables.md index 4267b65..c685436 100644 --- a/docs/dispensables.md +++ b/docs/dispensables.md @@ -1,8 +1,9 @@ -## **Dispensables** +# **Dispensables** -### Avoid comments +## Avoid comments Comments can become problematic in a few ways: + - If some code needs comments, it's a smell for deeper issues (e.g. bad variable naming, violation of single responsibility principle, poor abstraction) - Comments can grow stale and they can lie - Comments can make code even harder to understand when there's too much of it. @@ -24,9 +25,9 @@ if employee.isEligibleForBenefits(): # do something ``` -### Remove dead code +## Remove dead code -Dead code is code which is executed but whose result is never used in any other computation. Dead code is yet another unrelated thing that developers have to hold in our head when coding. It adds unnecessary cognitive load. +Dead code is code which is executed but whose result is never used in any other computation. Dead code is yet another unrelated thing that developers have to hold in our head when coding. It adds unnecessary cognitive load. If there's code which does not change the result of the program whether it runs or not, then it's not required for the code to run. We should remove it to keep the codebase clean. When we make small and frequent commits, we need not fear losing code. If we ever need those lines of code again, we can easily recover it from the git history. @@ -34,7 +35,6 @@ One common type of dead code are print statements (even glorified print statemen When our codebase has unit tests, the feedback from unit tests replaces the manual/visual feedback we used to use (e.g. df.head()) to check if things are working. This allows us to remove these print statements and make our code more readable. - **Bad:** ```python diff --git a/docs/functions.md b/docs/functions.md index 108a669..f489c61 100644 --- a/docs/functions.md +++ b/docs/functions.md @@ -1,6 +1,6 @@ -## **Functions** +# **Functions** -### Use functions to keep code "DRY" +## Use functions to keep code "DRY" The developer who learns to recognize duplication, and understands how to eliminate it through proper abstraction (i.e. defining the right functions or methods), can produce much cleaner code than one who continuously infects the application with unnecessary repetition. @@ -37,10 +37,10 @@ print(gaussian_accuracy) def train_model(ModelClass, X_train, Y_train, **kwargs): model = ModelClass(**kwargs) model.fit(X_train, Y_train) - + accuracy_score = round(model.score(X_train, Y_train) * 100, 2) print(f'accuracy ({ModelClass.__name__}): {accuracy_score}') - + return model, accuracy_score decision_tree_model, decision_tree_accuracy = train_model(DecisionTreeClassifier, X_train, Y_train) @@ -50,7 +50,8 @@ gaussian_model , gaussian_accuracy = train_model(GaussianNB, X_train, Y **Tip**: Notice how the symmetry of the 3 code blocks in the bad example made it easier for us to identify and refactor the duplicated code? One useful practice in eliminating duplication is to **first make the duplication as obvious as possible**. This makes it easier for us to identify opportunities for extracting the duplication into their appropriate homes. -### Functions should do one thing +## Functions should do one thing + This is by far the most important rule in software engineering. When functions do more than one thing, they are harder to compose, test, and reason about. When you can isolate a function to just one action, they can be refactored easily and your code will read much cleaner. If you take nothing else away from this guide other than this, you'll be ahead of many developers. **Bad:** @@ -83,7 +84,8 @@ def email_clients(clients: List[Client, ...]) -> None: Do you see an opportunity for using generators now? -**Even better** +**Even better**: + ```python def active_clients(clients: List[Client]) -> Generator[Client]: """Only active clients. @@ -98,8 +100,7 @@ def email_client(clients: Iterator[Client]) -> None: email(client) ``` - -### Functions should only be one level of abstraction +## Functions should only be one level of abstraction When you have more than one level of abstraction, your function is usually doing too much. Splitting up functions leads to reusability and easier testing. @@ -158,8 +159,7 @@ def parse(tokens: list) -> list: return syntax_tree ``` - -### Function names should say what they do +## Function names should say what they do **Bad:** @@ -185,10 +185,9 @@ message = Email() message.send() ``` +## Use type hints to improve readability -### Use type hints to improve readability - -Using type hints can make your code more readable and reasonable. +Using type hints can make your code more readable and reasonable. Your development experience will also be improved because your IDE will be able to give you better auto-complete suggestions about function/method names and parameters. **Bad:** @@ -203,10 +202,9 @@ With type hints, we can name our variables sensibly, and the IDE now offers bett type hints good example -It's important to note that type hints are meant to be entirely ignored by the Python runtime, and are checked only by 3rd party tools like `mypy` and Pycharm's integrated checker. You can read more about type hints and how to use type checkers [here](https://www.bernat.tech/the-state-of-type-hints-in-python/). - +It's important to note that type hints are meant to be entirely ignored by the Python runtime, and are checked only by 3rd party tools like `mypy` and Pycharm's integrated checker. You can read more about type hints and how to use type checkers [here](https://www.bernat.tech/the-state-of-type-hints-in-python/). -### Avoid side effects +## Avoid side effects A function produces a side effect if it does anything other than take a value in and return another value or values. For example, a side effect could be writing to a file, modifying some global variable, or accidentally wiring all your money to a stranger. @@ -215,7 +213,9 @@ Now, you do need to have side effects in a program on occasion - for example, li The main point is to avoid common pitfalls like sharing state between objects without any structure, using mutable data types that can be written to by anything, or using an instance of a class, and not centralizing where your side effects occur. If you can do this, you will be happier than the vast majority of other programmers. **Bad:** + + ```python # Global variable referenced by following function. # If another function used this name, now it'd be an array and could break. @@ -245,12 +245,11 @@ print(name) # 'Ryan McDermott' print(new_name) # ['Ryan', 'McDermott'] ``` +## Avoid unexpected side effects on values passed as function parameters -### Avoid unexpected side effects on values passed as function parameters - -We can unexpectedly change the values passed to our functions, even though our functions appear to be pure. +We can unexpectedly change the values passed to our functions, even though our functions appear to be pure. -This will happen when we pass non-primitive objects (e.g. lists, dictionaries, instances of classes, pandas dataframes) to a function because in Python (and indeed many other languages), non-primitive objects are [passed by reference](https://twitter.com/ericlbarnes/status/1138528829692174337). +This will happen when we pass non-primitive objects (e.g. lists, dictionaries, instances of classes, pandas dataframes) to a function because in Python (and indeed many other languages), non-primitive objects are [passed by reference](https://twitter.com/ericlbarnes/status/1138528829692174337). **Bad:** @@ -263,9 +262,9 @@ original = pd.DataFrame({ def multiply_column_by_10(df, column_name): df['multiplied column'] = df[column_name] * 10 - + return df - + new = multiply_column_by_10(original, 'values') original.head() # surprise! original dataframe is mutated and now it has @@ -283,16 +282,17 @@ original = pd.DataFrame({ def multiply_column_by_10(df, column_name): df = df.copy() df['multiplied column'] = df[column_name] * 10 - + return df - + new = multiply_column_by_10(original, 'values') original.head() # original dataframe is not mutated ``` -### Function arguments (2 or fewer ideally) -Limiting the amount of function parameters is incredibly important because it makes testing your function easier. Having more than three leads to a combinatorial explosion where you have to test tons of different cases with each separate argument. +## Function arguments (2 or fewer ideally) + +Limiting the amount of function parameters is incredibly important because it makes testing your function easier. Having more than three leads to a combinatorial explosion where you have to test tons of different cases with each separate argument. One or two arguments is ok, and three should be avoided. Anything more than that should be consolidated. Usually, if you have more than two arguments then your function is trying to do too much. In cases where it's not, most of the time a higher-level object will suffice as an argument. @@ -322,7 +322,8 @@ menu = Menu( ) ``` -**Also good** +**Also good**: + ```python class MenuConfig: """A configuration for the Menu. @@ -355,7 +356,8 @@ config.cancellable = True create_menu(config) ``` -**Fancy** +**Fancy**: + ```python from typing import NamedTuple @@ -389,7 +391,8 @@ create_menu( ) ``` -**Even fancier** +**Even fancier**: + ```python from dataclasses import astuple, dataclass @@ -423,10 +426,9 @@ create_menu( ) ``` +## Use default arguments instead of short circuiting or conditionals -### Use default arguments instead of short circuiting or conditionals - -**Tricky** +**Tricky**: Why write: @@ -447,8 +449,7 @@ def create_micro_brewery(name: str = "Hipster Brew Co."): # etc. ``` - -### Don't use flags as function parameters +## Don't use flags as function parameters Flags tell your user that this function does more than one thing. Functions should do one thing. Split your functions if they are following different code paths based on a boolean. diff --git a/docs/lesson-plan.md b/docs/lesson-plan.md index c44d62b..d9fb0c5 100644 --- a/docs/lesson-plan.md +++ b/docs/lesson-plan.md @@ -1,53 +1,66 @@ # clean-code-ml lesson plan Target audience: + - Data scientists / data science enthusiasts Pre-requisites for attendees: + - Some basic experience with Python, Jupyter notebook, `scikit-learn`, and `pandas` -### Session outline +## Rules of thumb + +- Remember the [Two Hats](https://www.martinfowler.com/articles/workflowsOfRefactoring/#2hats) +- Keep tests in watch mode. Test continuously during each refactoring +- Make small and frequent commits + +## Session outline + - Temperature check - - Python, jupyter, ML, unit testing - - Shuffle seats if necessary + - Python, jupyter, ML, unit testing + - Shuffle seats if necessary - Why this session? What's wrong with jupyter notebooks (Answer: change and complexity) - - Jargon: - - code smells ("if it stinks, change it", Grandma Beck) - - go through cheat-sheet.md - - refactoring + - Jargon: + - code smells ("if it stinks, change it", Grandma Beck) + - go through [cheat-sheet.md](./cheat-sheet.md) + - refactoring - Code smell #1 (print statements) - - First, show the titanic dataset and what the notebook is doing - - Starter code: [notebook-0](../notebooks/titanic-notebook-0.ipynb) - - Look at this, and call out the first code smell that comes to your mind - - Refactored code: [notebook-1](../notebooks/titanic-notebook-1.ipynb) + - First, show the titanic dataset and what the notebook is doing + - Starter code: [notebook-0](../notebooks/titanic-notebook-0.ipynb) + - Look at this, and call out the first code smell that comes to your mind + - Refactored code: [notebook-1](../notebooks/titanic-notebook-1.ipynb) + - Task: Remove code that is only for printing (e.g. `print(...)`, `df.head()`, etc.) + - Task: Remove dead code (e.g. `df[['IsAlone', 'Survived']].groupby(['IsAlone'], as_index=False).mean()`) - Code smell #2 (exposed internals) - - Starter code: [notebook-1](../notebooks/titanic-notebook-1.ipynb) - - Refactored code: [notebook-2](../notebooks/titanic-notebook-2.ipynb) - - Use [train.py](https://github.com/davified/clean-code-ml/blob/master/src/train.py) to show the value of functions / abstractions + - Starter code: [notebook-1](../notebooks/titanic-notebook-1.ipynb) + - Refactored code: [notebook-2](../notebooks/titanic-notebook-2.ipynb) + - Use [train.py](https://github.com/davified/clean-code-ml/blob/master/src/train.py) to show the value of functions / abstractions + - Task: None - Code smell #3 (Duplication) - - DEMO + EXERCISE 1: How to refactor to functions - train_model() - - Starter code: [notebook-2](../notebooks/titanic-notebook-2.ipynb) - - Refactored code: [notebook-3](../notebooks/titanic-notebook-3.ipynb) -- Break - - If you cannot run nosetests or start jupyter notebook, please ask us for help. + - DEMO + EXERCISE 1: How to refactor to functions - train_model() + - Starter code: [notebook-2](../notebooks/titanic-notebook-2.ipynb) + - Refactored code: [notebook-3](../notebooks/titanic-notebook-3.ipynb) +- Break + - If you cannot run nosetests or start jupyter notebook, please ask us for help. - Code smell #4 (No unit tests): - - Starter code: [notebook-3](../notebooks/titanic-notebook-3.ipynb). What are any other code smells? - - Demo + codealong: How to write a unit test + arrange/act/assert - - Demo + codealong: How to write a unit test with dataframes - - Demo + exercise 2 - - Exercise 3 + - Starter code: [notebook-3](../notebooks/titanic-notebook-3.ipynb). What are any other code smells? + - Demo + codealong: How to write a unit test + arrange/act/assert + - Demo + codealong: How to write a unit test with dataframes + - Demo + exercise 1.5: How to write a characterisation test + metrics test + - Demo + exercise 2 + - Exercise 3 - Conclusion - - Show final solution (ask for 1 volunteer) - - Show [titanic-notebook-solution](https://github.com/davified/clean-code-ml/blob/master/notebooks/titanic-notebook-solution.ipynb) and [`src/train.py`](https://github.com/davified/clean-code-ml/blob/master/src/train.py) - - Full list of [code smells](../README.md)? - - Take-home reading: [refactoring process](./refactoring-process.md) + - Show [titanic-notebook-solution](https://github.com/davified/clean-code-ml/blob/master/notebooks/titanic-notebook-solution.ipynb) and [`src/train.py`](https://github.com/davified/clean-code-ml/blob/master/src/train.py) + - Full list of [code smells](../README.md) + - Take-home reading: [refactoring process](./refactoring-process.md) - Bonus: CI -### Note to self (david) +## Note to self (david) + - Set up split screen to reduce toggling - Write session plan (with time) on the wall and track progress as we go - Share google doc scratchpad with class: https://tinyurl.com/y42xa86r -- Get feedback on - - code smells in clean-code-ml - - refactoring notebook kata - - workshop (any general feedback) +- Get feedback on + - code smells in clean-code-ml + - refactoring notebook kata + - workshop (any general feedback) diff --git a/docs/refactoring-exercise.md b/docs/refactoring-exercise.md index 5b3595c..e227928 100644 --- a/docs/refactoring-exercise.md +++ b/docs/refactoring-exercise.md @@ -1,51 +1,39 @@ -# clean-code-ml refactoring exercise +# clean-code-ml refactoring exercise setup -## Pre-workshop setup +## Prerequisites Please ensure you have the following: + - a [GitHub](https://github.com/) account - a [CircleCI](https://circleci.com) account - an IDE ([VS Code](https://code.visualstudio.com/Download) or [PyCharm](https://www.jetbrains.com/pycharm/download/)) -- Windows users: - - Download [Git Bash](https://gitforwindows.org/) +- [Windows Users only] Install [git bash](https://gitforwindows.org/). We will be using `git bash` as the terminal for the workshop. ## Getting started 1. Fork repo 1. Clone repository: `git clone https://github.com/YOUR_USERNAME/clean-code-ml` -1. Run `bin/setup.sh`. This will install miniconda3 if it's not already installed, and install project-level dependencies specified in `./environment.yml` - -You're ready to roll! Here are some common commands that you can run in your dev workflow. - -```shell -# activate virtual environment -source activate clean-code-ml +1. Project setup. You can either use docker or conda. Choose whichever you prefer: -# deactivate virtual environment -conda deactivate +- docker ([workshop setup instructions](./setup-docker.md)) +- conda ([workshop setup instructions](./setup-conda.md)) + - Mac/Linux users + - Windows users -# run unit tests -nosetests - -# run unit tests in watch mode and color output -nosetests --with-watch --rednose --nologcapture - -# start jupyter notebook server -jupyter notebook -# Now you can visit localhost:8888 on your browser. -``` +If you encounter any errors, please refer to [FAQs](./FAQs.md) for a list of common errors and how to fix them. ## IDE configuration Configure your IDE to use `~/miniconda3/envs/clean-code-ml/bin/python` as the Python interpreter. Here are the instructions on how to do that in [VS Code](https://code.visualstudio.com/docs/python/environments) and [PyCharm](https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html). Once you've done that, you should be able to: + 1. Get helpful auto-complete suggestions in your IDE as you type. If somehow that's not showing up, try restarting your code editor. 1. Let your IDE auto-format your code in a file. We've installed [autopep8](https://github.com/hhatto/autopep8) using conda, and now your IDE can help you with the auto-formatting) - - To do this in VS Code, hit `Shift` + `⌘` + `F` + - To do this in VS Code, hit `Shift` + `⌘` + `F` 1. Use other tools provided by your IDE. - - For VS Code, hit `F1` and type 'Python Refactor' and you can experiment with any of these commands (e.g. 'Sort Imports') + - For VS Code, hit `F1` and type 'Python Refactor' and you can experiment with any of these commands (e.g. 'Sort Imports') ## Attributions -The notebook which we use for the starting point of our refactoring exercise was adapted/modified from a [Kaggle submission](https://www.kaggle.com/bhaveshsk/getting-started-with-titanic-dataset/data) for the titanic competition. \ No newline at end of file +The notebook which we use for the starting point of our refactoring exercise was adapted/modified from a [Kaggle submission](https://www.kaggle.com/bhaveshsk/getting-started-with-titanic-dataset/data) for the titanic competition. diff --git a/docs/refactoring-process.md b/docs/refactoring-process.md index c1b91d3..077111a 100644 --- a/docs/refactoring-process.md +++ b/docs/refactoring-process.md @@ -10,40 +10,50 @@ Nonetheless, since this is the predicament which we find ourselves in, we need t ### The refactoring process +refactoring cycle. how to refactor a jupyter notebook + **Before we start refactoring:** - Ensure that notebooks when run from start to end - - Even better if you can add an "integration" or "functional" test of the code (see last step - "Add a functional test for the ML model") - Make a copy of the original notebook (for comparing the end result later) - - This frees us up from any emotional attachment and allow us to ruthlessly clean up the code. + - This frees us up from any emotional attachment and allow us to ruthlessly clean up the code. +- Convert Jupyter notebook into a plain Python file + - This will allow you to have all the benefits of using an IDE (e.g. autocomplete, intellisense, formatting, auto-renaming, linting, keyboard shortcuts, etc. For more details, check out this [tutorial](https://www.youtube.com/watch?v=KUvqDINDzFE&list=PLO9pkowc_99ZhP2yuPU8WCfFNYEx2IkwR&index=6)) + - Command: `jupyter nbconvert --to script mynotebook.ipynb` - Remove print statements (e.g. `print(...)`, `df.head()`, `df.describe()`, `df.plot()`) - - This removes noise and visual clutter, and makes the next step exponentially easier + - This removes noise and visual clutter, and makes the next step exponentially easier - Read notebook and list [code smells](../README.md) - +- Define refactoring boundary and add a characterisation test + - This is arguably **the most important step**. Having a characterisation test will give you fast feedback because it can be run continuously as you refactor. It will tell you within seconds (or even milliseconds) if you've accidentally introduced an error or a bug. + - Without the characterisation test, you would have to manually restart and rerun the entire Jupyter notebook - which is cumbersome and so 2019. **The refactoring cycle:** The instructions for how to run each step can be found in the [refactoring exercise](./refactoring-exercise.md) -1. Identify a block of code that can be extracted into a pure function -1. Pseudo-TDD (test-driven development) - - - Run the unit tests in [watch mode](./refactoring-exercise.md#getting-started) - - Write a [unit test](../src/tests/test_preprocessing.py) for the code block - - Create a [Python module](../src/preprocessing.py) and define a function. Move existing implementation from notebook into that function - - Make the test pass -1. In the notebook, replace original code block with the newly defined function -1. Restart and run entire notebook (Unfortunately, until we have sufficient unit tests, we will still need manual ā€œintegrationā€ tests for the time being.) -1. [If possible] Refactor function some more -1. Commit your changes - - git add . - - git commit -m "your commit message" -1. **Rinse and repeat** +- Identify a block of code that can be extracted into a pure function +- Write automated tests + + - Run the unit tests in [watch mode](./refactoring-exercise.md#getting-started) + - Write a [unit test](../src/tests/test_preprocessing.py) for the code block + - Create a [Python module](../src/preprocessing.py) and define a function. Move existing implementation from notebook into that function + - Make the test pass + +- In the notebook, replace original code block with the newly defined function +- Ensure characterisation tests are still passing +- [If possible] Refactor function some more +- Commit your changes + - `git add .` + - `git commit -m "your commit message"` +- **Rinse and repeat** ### Some tips -- When refactoring, don't change the program's observable behaviour. -- Keep tests in watch mode. Test after every refactoring - - "Testing after each change means that when I make a mistake, I only have a small change to consider in order to spot the error, which makes it far easier to find and fix." (Martin Fowler) + +- When refactoring, don't change the program's observable behaviour. (Remember the [Two Hats](https://www.martinfowler.com/articles/workflowsOfRefactoring/#2hats)) +- Keep tests in watch mode. Test continuously during each refactoring + - "Testing after each change means that when I make a mistake, I only have a small change to consider in order to spot the error, which makes it far easier to find and fix." (Martin Fowler) - Make small and frequent commits - Don't try big bang refactorings. Refactoring isn’t an activity that’s separated from programming. We refactor as we go. - When to refactor - - "**Three strikes, then you refactor.** The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor." (Martin Fowler) + - "**Three strikes, then you refactor.** The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor." (Martin Fowler) + + diff --git a/docs/refactoring-video.md b/docs/refactoring-video.md new file mode 100644 index 0000000..7f783a4 --- /dev/null +++ b/docs/refactoring-video.md @@ -0,0 +1,27 @@ +# Tutorial: How to refactor a jupyter notebook + +My refactoring process (use them in the order that works for you) + +## Prerequisites +- [x] Local dev environment is set up +- [x] Run notebook and ensure it works +- [x] Make a copy of the notebook (to eliminate any emotional attachment) +- [x] Remove print statements +- [x] Read notebook and list code smells +- [x] Convert notebook as Python file +- [x] Determine boundary of refactoring and add characterisation test there + +## Refactoring steps +- [] Run tests in watch mode +- [] Identify a block of code that can be extracted into a pure function +- [] The refactoring cycle + - [] Write a test + - [] Create a Python module and define a function. + - [] Make the test pass + - [] In the notebook, replace original code block with the newly defined function + - [] Restart and run entire notebook + - (Optional) Refactor function some more + - [] Commit your changes +- [] Add functional test for ML model +- [] Celebrate 🤘🤘 + diff --git a/docs/setup-conda.md b/docs/setup-conda.md new file mode 100644 index 0000000..c66983e --- /dev/null +++ b/docs/setup-conda.md @@ -0,0 +1,100 @@ +# Workshop Setup (Using Conda) + +## Setup instructions (Mac / Linux) + +1. Run `bin/setup.sh`. This will install miniconda3 if it's not already installed, and install project-level dependencies specified in `./environment.yml` + +You're ready to roll! Here are some common commands that you can run in your dev workflow. + +```shell +# initialize conda shell +conda init # SHELL_NAME options: bash, zsh, fish, powershell + +# close and open your shell/terminal + +# activate virtual environment +conda activate clean-code-ml + +# run unit tests +nosetests + +# run unit tests in watch mode and color output +nosetests --with-watch --rednose --nologcapture + +# open a new terminal and start jupyter notebook server +jupyter notebook +# Now you can visit localhost:8888 on your browser. + +# at the end of the session, deactivate virtual environment +conda deactivate + +``` + +## Setup instructions (Windows) + +### Install Anaconda (if you haven't installed it before) + +- Visit: https://docs.conda.io/en/latest/miniconda.html +- Click on Miniconda3 Windows 64-bit to download the installer +- Run the installer. Go with the defaults, but **make sure to check 'Make Anaconda the default Python'"** + +### Install project-level dependencies + +Note: If you get an error (`bash: conda: command not found`), please run the steps in the **Troubleshooting** section below and try these steps again: + +```shell +# install create conda environment and install dependencies +conda create -f ./environment.yml + +# activate virtual environment +conda activate clean-code-ml + +# run unit tests +nosetests + +# run unit tests in watch mode and color output +nosetests --with-watch --rednose --nologcapture + +# open a new terminal and start jupyter notebook server +jupyter notebook +# Now you can visit localhost:8888 on your browser. + +# at the end of the session, deactivate virtual environment +conda deactivate +``` + +## Troubleshooting `conda: command not found` error + +1. Find full path to conda executable: + +- search for conda.exe in Windows Search bar +- In the file that is found, click on 'Open file location' +- Step back to the Miniconda3 folder +- Navigate into Miniconda3/Scripts folder +- Copy full path to your git bash terminal, and replace Windows backslashes with Unix forward slashes. Example: + - `C:\Users\myname\Miniconda3\Scripts\conda.exe` -> `/c/Users/myname/Miniconda3/Scripts/conda.exe` +- Use `/c/Users/myname/Miniconda3/Scripts/conda.exe` instead of the `conda` shorthand: + +2. Configure `conda` to be a command that you can use in your terminal + +```shell +# verify if conda can be used directly. If you see 'which: no conda in ...', fix it using the steps below +which conda + +# initialize conda shell +/c/Users/myname/Miniconda3/Scripts/conda.exe init bash # we choose bash because gitbash is a bash shell + +# reload the shell for conda init to take effect +source ~/.bash_profile + +``` + +3. **Important**: If you get the same `conda: command not found` error again in any new shell (e.g. in VS Code), simply run `source ~/.bash_profile` again + +## Bonus: running test coverage + +We've installed a python library ([`coverage`](https://coverage.readthedocs.io/en/coverage-5.0.3/)) that tells you which lines of code are tested/not tested. To do use, run: + +- `coverage run -m nose` +- `coverage html` +- Open `clean-code-ml/htmlcov/index.html` in your browser diff --git a/docs/setup-docker.md b/docs/setup-docker.md new file mode 100644 index 0000000..54fe1dc --- /dev/null +++ b/docs/setup-docker.md @@ -0,0 +1,92 @@ +# Workshop Setup (Using Docker) + +## Prerequisites + +1. [For Windows users] Docker Desktop requires Windows 10 Pro or Enterprise version 15063 to run. If your laptop is neither of these, please use [conda](./setup-conda.md) instead of Docker. +1. Install Docker + +- [for Mac](https://docs.docker.com/docker-for-mac/install/) +- [for Linux](https://docs.docker.com/install/linux/docker-ce/ubuntu/) +- [for Windows](https://docs.docker.com/docker-for-windows/install/) +- **Important things to note**: + - You will be prompted to create a DockerHub account. Follow the instructions in order to download Docker + - Follow the installation prompts (go with the default options) **until you have successfully started Docker** + - [Windows users] When prompted to enable Hyper-V and Containers features, click 'Ok' and let computer restart again. + - You may have to restart your computer 2-3 times. + +2. Start Docker on your desktop (Note: Wait for Docker to complete startup before running the subsequent commands. You'll know when startup is completed when the docker icon in your taskbar stops animating) + +## Setup (Mac / Linux) + +```shell +# ensure you are in clean-code-ml directory + +# build docker image +docker build . -t clean-code-ml + +# start bash shell in a docker container +docker run -it -v $(pwd):/code -p 8888:8888 clean-code-ml bash +``` + +## Setup (Windows) + +```shell +# ensure you are in clean-code-ml directory + +# build docker image +MSYS_NO_PATHCONV=1 docker build . -t clean-code-ml + +# start bash shell in a docker container +winpty docker run -it -v C:\\Users\\path\\to\\your\\clean-code-ml:/code -p 8888:8888 clean-code-ml bash +# Note: to find the path, you can run `pwd` in git bash, and manually replace forward slashes (/) with double backslashes (\\) +``` + +## Let's roll + +Now that your docker container is up and running, you're ready to run the commands that we'll use in this workshop + +```shell +# run unit tests +nosetests + +# run unit tests in watch mode and color output +nosetests --with-watch --rednose --nologcapture + +# open a new terminal and start jupyter notebook server +jupyter notebook --ip 0.0.0.0 --no-browser --allow-root + +# To stop a process in the docker container, hit Ctrl + C +``` + +Now you're ready to roll! + +## Other useful docker commands + +To run 2 commands/processes in the same container (e.g. run jupyter notebook and nosetests at the same time), you can start a second process in a running container by doing the following: + +```shell +# See list of running containers +docker ps + +# Start a bash shell in a running container when it’s running +docker exec -it bash +# Now you can run your second command (e.g. start jupyter notebook) +``` + +## Configure your IDE for better intellisense + +1. install dependencies locally (i.e. outside of Docker image): `bin/install_deps_locally.sh` + +2. Select Python interpreter. In VS Code: + +- Open command palette: Press `F1` +- Type: "Python: Select Interpreter" +- Choose or type: `./.venv/bin/python` + +## Bonus: running test coverage + +We've installed a python library ([`coverage`](https://coverage.readthedocs.io/en/coverage-5.0.3/)) that tells you which lines of code are tested/not tested. To do use, run: + +- `coverage run -m nose` +- `coverage html` +- Open `clean-code-ml/htmlcov/index.html` in your browser diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..ca7a2fb --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,18 @@ +# Video Series: Coding Habits for Data Scientists + +## Series outline + +- Project setup + - [done] Docker + - [future]: Plain Python setup and Conda +- Automated testing + - [to-edit] Unit testing 101 + Testing dataframe transformations + - [todo] ML functional tests / characterisation tests +- [done] CI +- [to-edit] Become more productive with your IDE + - Configuring virtual environment in IDE + - Formatting + - Linting + - Refactoring +- [done] Code smells +- [todo] How to refactor a jupyter notebook \ No newline at end of file diff --git a/docs/variables.md b/docs/variables.md index 4e74428..173c3f5 100644 --- a/docs/variables.md +++ b/docs/variables.md @@ -1,9 +1,10 @@ -## **Variables** +# **Variables** -### Variable names should reveal intent -We will read more code than we will ever write. It's important for our code to express intent so that our readers don't have to waste mental effort to figure out puzzles. +## Variable names should reveal intent -One common culprit in data science code is dataframes. Every dataframe is named as `df`. In software programming, it's an unusual (and bad) practice to embed information about variable types in the variable name (e.g. we would probably never write `string = 'Hello friends'`. Instead, we would write `greeting = 'Hello friends'`). +We will read more code than we will ever write. It's important for our code to express intent so that our readers don't have to waste mental effort to figure out puzzles. + +One common culprit in data science code is dataframes. Every dataframe is named as `df`. In software programming, it's an unusual (and bad) practice to embed information about variable types in the variable name (e.g. we would probably never write `string = 'Hello friends'`. Instead, we would write `greeting = 'Hello friends'`). **Bad:** @@ -31,7 +32,7 @@ total_loan_amount = monthly_loans_in_december.sum() ``` -### Use meaningful and pronounceable variable names +## Use meaningful and pronounceable variable names **Bad:** @@ -45,10 +46,11 @@ ymdstr = datetime.date.today().strftime("%y-%m-%d") current_date: str = datetime.date.today().strftime("%y-%m-%d") ``` -### Use the same vocabulary for the same type of variable +## Use the same vocabulary for the same type of variable **Bad:** Here we use three different names for the same underlying entity: + ```python get_user_info() get_client_data() @@ -57,13 +59,14 @@ get_customer_record() **Good**: If the entity is the same, you should be consistent in referring to it in your functions: + ```python get_user_info() get_user_data() get_user_record() ``` -### Avoid magic numbers and magic numbers +## Avoid magic numbers and magic numbers **Bad:** @@ -81,7 +84,8 @@ SECONDS_IN_A_DAY = 86400 time.sleep(SECONDS_IN_A_DAY) ``` -### Use variables to keep code "DRY" ("Don't Repeat Yourself") +## Use variables to keep code "DRY" ("Don't Repeat Yourself") + DRY stands for "Don't Repeat Yourself". If you find yourself changing the same thing in multiple places, then that thing which you're changing is a candidate for refactoring. **Bad:** @@ -102,7 +106,8 @@ loans = loans.fillna({target_column: 0}) loans.groupby([target_column]).mean().sort_values(by=target_column) ``` -### Use explanatory variables +## Use explanatory variables + **Bad:** ```python @@ -129,6 +134,7 @@ save_city_zip_code(city, zip_code) **Good**: Decrease dependence on regex by naming subpatterns. + ```python address = 'One Infinite Loop, Cupertino 95014' city_zip_code_regex = r'^[^,\\]+[,\\\s]+(?P.+?)\s*(?P\d{5})?$' @@ -137,7 +143,8 @@ matches = re.match(city_zip_code_regex, address) save_city_zip_code(matches['city'], matches['zip_code']) ``` -### Avoid mental mapping +## Avoid mental mapping + Don’t force the reader of your code to translate what the variable means. Explicit is better than implicit. @@ -166,8 +173,7 @@ for location in locations: dispatch(location) ``` - -### Don't add unneeded context +## Don't add unneeded context If your class/object name tells you something, don't repeat that in your variable name. @@ -199,4 +205,4 @@ car = Car() car.make car.model car.color -``` \ No newline at end of file +``` diff --git a/environment.yml b/environment.yml index f25ee21..2636b6b 100644 --- a/environment.yml +++ b/environment.yml @@ -13,3 +13,7 @@ dependencies: - nose==1.3.7 - nose-watch==0.9.2 - rednose==1.3.0 + - rope==0.16.0 + - PyYAML==5.3 + - argh==0.26.2 + - coverage==5.0.3 diff --git a/images/command_palette.png b/images/command_palette.png new file mode 100644 index 0000000..8e80893 Binary files /dev/null and b/images/command_palette.png differ diff --git a/images/python_environment.png b/images/python_environment.png new file mode 100644 index 0000000..4f48245 Binary files /dev/null and b/images/python_environment.png differ diff --git a/images/refactoring_cycle.png b/images/refactoring_cycle.png new file mode 100644 index 0000000..931971d Binary files /dev/null and b/images/refactoring_cycle.png differ diff --git a/notebooks/titanic-notebook-0.ipynb b/notebooks/titanic-notebook-0.ipynb index 72f970a..098dbca 100644 --- a/notebooks/titanic-notebook-0.ipynb +++ b/notebooks/titanic-notebook-0.ipynb @@ -5043,7 +5043,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.6.10" } }, "nbformat": 4, diff --git a/notebooks/titanic-notebook-1.ipynb b/notebooks/titanic-notebook-1.ipynb index 34db09e..195b797 100644 --- a/notebooks/titanic-notebook-1.ipynb +++ b/notebooks/titanic-notebook-1.ipynb @@ -724,9 +724,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.6.10" } }, "nbformat": 4, "nbformat_minor": 1 -} +} \ No newline at end of file diff --git a/notebooks/titanic-notebook-1.py b/notebooks/titanic-notebook-1.py new file mode 100644 index 0000000..58ec5a6 --- /dev/null +++ b/notebooks/titanic-notebook-1.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python +# coding: utf-8 + +# In[1]: + + +#source: https://www.kaggle.com/bhaveshsk/getting-started-with-titanic-dataset/data +#data analysis and wrangling +import pandas as pd +import numpy as np +import random as rnd + +#data visualization +import seaborn as sns +import matplotlib.pyplot as plt +get_ipython().run_line_magic('matplotlib', 'inline') + +#machine learning packages +from sklearn.linear_model import LogisticRegression +from sklearn.svm import SVC +from sklearn.ensemble import RandomForestClassifier +from sklearn.neighbors import KNeighborsClassifier +from sklearn.naive_bayes import GaussianNB +from sklearn.linear_model import Perceptron +from sklearn.linear_model import SGDClassifier +from sklearn.tree import DecisionTreeClassifier +from sklearn import metrics + + +# In[2]: + + +train_df = pd.read_csv("../input/train.csv") +test_df = pd.read_csv("../input/test.csv") +df = pd.concat([train_df,test_df]) + +df.head() + + +# In[3]: + + +df = df.drop(['Ticket', 'Cabin'], axis=1) + + +# In[4]: + + +df['Title'] = df.Name.str.extract(' ([A-Za-z]+)\.', expand=False) + + +# In[5]: + + +df['Title'] = df['Title'].replace(['Lady', 'Countess','Capt', 'Col','Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare') + +df['Title'] = df['Title'].replace('Mlle', 'Miss') +df['Title'] = df['Title'].replace('Ms', 'Miss') +df['Title'] = df['Title'].replace('Mme', 'Mrs') + + +# In[6]: + + +title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5} + +df['Title'] = df['Title'].map(title_mapping) +df['Title'] = df['Title'].fillna(0) + + +# In[7]: + + +df = df.drop(['Name', 'PassengerId'], axis=1) + + +# In[8]: + + +df['Sex'] = df['Sex'].map( {'female': 1, 'male': 0} ).astype(int) + + +# In[9]: + + +df['Age'] = df['Age'].fillna(df['Age'].dropna().median()) + + +# In[10]: + + +df['AgeBand'] = pd.cut(df['Age'], 5) + + +# In[11]: + + +df.loc[ df['Age'] <= 16, 'Age'] = 0 +df.loc[(df['Age'] > 16) & (df['Age'] <= 32), 'Age'] = 1 +df.loc[(df['Age'] > 32) & (df['Age'] <= 48), 'Age'] = 2 +df.loc[(df['Age'] > 48) & (df['Age'] <= 64), 'Age'] = 3 + + +# In[12]: + + +df = df.drop(['AgeBand'], axis=1) + + +# In[13]: + + +df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 + + +# In[14]: + + +df['IsAlone'] = 0 +df.loc[df['FamilySize'] == 1, 'IsAlone'] = 1 + + +# In[15]: + + +df = df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1) + + +# In[16]: + + +df['Age*Class'] = df.Age * df.Pclass + + +# In[17]: + + +freq_port = df.Embarked.dropna().mode()[0] + + +# In[18]: + + +df['Embarked'] = df['Embarked'].fillna(freq_port) + + +# In[19]: + + +df['Embarked'] = df['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int) + + +# In[20]: + + +df['Fare'] = df['Fare'].fillna(df['Fare'].dropna().median()) + + +# In[21]: + + +df['FareBand'] = pd.qcut(df['Fare'], 4) + + +# In[22]: + + +df.loc[ df['Fare'] <= 7.91, 'Fare'] = 0 +df.loc[(df['Fare'] > 7.91) & (df['Fare'] <= 14.454), 'Fare'] = 1 +df.loc[(df['Fare'] > 14.454) & (df['Fare'] <= 31), 'Fare'] = 2 +df.loc[ df['Fare'] > 31, 'Fare'] = 3 +df['Fare'] = df['Fare'].astype(int) + +df = df.drop(['FareBand'], axis=1) + + +# In[23]: + + +train_df = df[-df['Survived'].isna()] +test_df = df[df['Survived'].isna()] +test_df = test_df.drop('Survived', axis=1) + + +# In[24]: + + +X_train = train_df.drop("Survived", axis=1) +Y_train = train_df["Survived"] +X_test = test_df.copy() + + +# In[25]: + + +svc = SVC() +svc.fit(X_train, Y_train) +Y_pred = svc.predict(X_test) +acc_svc = round(svc.score(X_train, Y_train) * 100, 2) +acc_svc + + +# In[26]: + + +knn = KNeighborsClassifier() +knn.fit(X_train, Y_train) +Y_pred = knn.predict(X_test) +acc_knn = round(knn.score(X_train, Y_train) * 100, 2) +acc_knn + + +# In[27]: + + +gaussian = GaussianNB() +gaussian.fit(X_train, Y_train) +Y_pred = gaussian.predict(X_test) +acc_gaussian = round(gaussian.score(X_train, Y_train) * 100, 2) +acc_gaussian + + +# In[28]: + + +perceptron = Perceptron() +perceptron.fit(X_train, Y_train) +Y_pred = perceptron.predict(X_test) +acc_perceptron = round(perceptron.score(X_train, Y_train) * 100, 2) +acc_perceptron + + +# In[29]: + + +sgd = SGDClassifier() +sgd.fit(X_train, Y_train) +Y_pred = sgd.predict(X_test) +acc_sgd = round(sgd.score(X_train, Y_train) * 100, 2) +acc_sgd + + +# In[30]: + + +decision_tree = DecisionTreeClassifier() +decision_tree.fit(X_train, Y_train) +Y_pred = decision_tree.predict(X_test) +acc_decision_tree = round(decision_tree.score(X_train, Y_train) * 100, 2) +acc_decision_tree + + +# In[31]: + + +random_forest = RandomForestClassifier(n_estimators=100) +random_forest.fit(X_train, Y_train) +Y_pred = random_forest.predict(X_test) +random_forest.score(X_train, Y_train) +acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2) +acc_random_forest + + +# In[32]: + + +models = pd.DataFrame({ + 'Model': ['Support Vector Machines', 'KNN', + 'Random Forest', 'Naive Bayes', 'Perceptron', + 'Stochastic Gradient Decent', + 'Decision Tree'], + 'Score': [acc_svc, acc_knn, + acc_random_forest, acc_gaussian, acc_perceptron, + acc_sgd, acc_decision_tree]}) +models.sort_values(by='Score', ascending=False) + + +# In[ ]: + + + + diff --git a/notebooks/titanic-notebook-refactoring-starter.ipynb b/notebooks/titanic-notebook-refactoring-starter.ipynb index 3e89017..b724a8f 100644 --- a/notebooks/titanic-notebook-refactoring-starter.ipynb +++ b/notebooks/titanic-notebook-refactoring-starter.ipynb @@ -580,7 +580,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.6.10" } }, "nbformat": 4, diff --git a/requirements.txt b/requirements.txt index c1ffd6a..9854472 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,17 @@ -autopep8 +black==19.10b0 jupyter==1.0.0 matplotlib==3.0.2 mypy==0.701 -nose==1.3.7 nose-watch==0.9.2 +nose==1.3.7 numpy==1.15.4 pandas==0.23.4 +pylint==2.4.4 rednose==1.3.0 +rope==0.16.0 scikit-learn==0.21.2 -seaborn==0.9.0 \ No newline at end of file +seaborn==0.9.0 + +# nose-watch's dependencies +PyYAML==5.3 +argh==0.26.2 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/test_model_metrics.py b/src/tests/test_model_metrics.py index 9991f72..989aa25 100644 --- a/src/tests/test_model_metrics.py +++ b/src/tests/test_model_metrics.py @@ -10,7 +10,7 @@ def test_model_precision_score_should_be_above_threshold(self): precision = precision_score(Y_test, Y_pred) - self.assertGreaterEqual(precision, 0.7) + self.assertGreaterEqual(precision, 0.6) def test_model_recall_score_should_be_above_threshold(self): model, X_test, Y_test = prepare_data_and_train_model()