Training a CNN to Classify LEGOs

This article is part of a series. It should explain the code used to train our convolutional neural-network (CNN) LEGO classifier.

If you want to code along with this article, we’ve made it available in Google’s Colab:

Or if you want to run the code locally:

It’s a WIP, so comment below if you run into any issues.

Classifier Code:

Our code started with a notebook found on Kaggle:

However, there problems in the code. I rewrote most of it, so I’m not sure how much of the original is left. Still, cite your sources!

Some of the issues were:

  • It used a model more complex than needed.
  • The code format was a mess.
  • Mismatch of target output and loss.

It was the last one which is super tricky, but critical. It’s a hard to catch bug which inaccurately reports high accuracy. I’ll discuss it more below, but it’s a trap I’ve fallen into myself. Regardless of the issues, it was good jump-starter code, since we’ve never worked with a CNN.

Project Setup (local only)

If you are running this code locally, you will need to do the following.

Enter the command prompt and navigate to your home directory. We’re going to clone the project repository (repo), then, clone the data repo inside the project folder.

git clone https://github.com/Ladvien/lego_sorter.git
cd lego_sorter
git clone https://github.com/Ladvien/lego_id_training_data.git

Then, open your Python IDE, set your directory to ./lego_sorter, and open lego_classifier_gpu.py.

Lastly, if you see a cell like this:

!git clone https://github.com/Ladvien/lego_id_training_data.git
!mkdir ./data
!mkdir ./data/output
!ls

Skip or delete them, they are need when running the Colab notebook. Of course, if you are running the Colab notebook, make sure to execute them.

Classifier Code: Needed Libraries

Below is the code we used. Reviewing it, I see some ways to clean it up, so know it may change in the future.

Here’s a breakdown of why the libraries are needed:

  • tensorflow – Google’s main deep-learning library, it’s the heart of the project.
  • keras – a library abstracting a lot of the details from creating a machine learning model.
  • json – we write the classes to file for use later.
  • tensorboard – a library for visualizing your training session.
  • webbrowser – this is opens your webrowser to Tensorboard.

# Import needed tools.
import os
import matplotlib.pyplot as plt
import json
import numpy as np
from scipy import stats

# Import Keras
import tensorflow as tf
import tensorflow.keras
from tensorflow.keras.layers import Dense,Flatten, Dropout, Lambda
from tensorflow.keras.layers import SeparableConv2D, BatchNormalization, MaxPooling2D, Conv2D, Activation
from tensorflow.compat.v1.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, TensorBoard, CSVLogger, ReduceLROnPlateau
from tensorflow.keras.preprocessing import image

# Tensorboard
from tensorboard import program
import webbrowser
import time

If you are following along with this code locally and need help setting up these libraries, just drop a comment below. I got you.

Classifier Code: Parameters

The parameters sections is the heart of the training, I’ll explain what the parameters are doing and highlight those you might want to tweak.

continue_training       = False
initial_epoch           = 0
clear_logs              = True

input_shape             = (300, 300, 3) # This is the shape of the image width, length, colors
image_size              = (input_shape[0], input_shape[1]) # DOH! image_size is (height, width)
train_test_ratio        = 0.2
zoom_range              = 0.1
shear_range             = 0.1

# Hyperparameters
batch_size              = 16
epochs                  = 40
steps_per_epoch         = 400
validation_steps        = 100 
optimizer               = 'adadelta' 
learning_rate           = 1.0
val_save_step_num       = 1

path_to_graphs          = './data/output/logs/'
model_save_dir          = './data/output/'
train_dir               = './lego_id_training_data/gray_train/'
val_dir                 = './lego_id_training_data/gray_test/'

Parameters: Training Session

The first few parameters help continue from an interrupted training session. For example, if your session is interrupted at epoch 183, then you could set continue_training = True and initial_epoch = 184, then execute the script. This should then load the last best model and pick back up training where you left off. Lastly, if you set clear_logs = True then it clears the Tensorboard information. So, if you continue a session, you will want to set this to False.

This section is a WIP and there are several issues. First, the Tensorboard logs should be saved in separate folders and shouldn’t need to be cleared. Also, when continuing a training session, it resets the best validation score (tracked for saving your model before overfitting) resulting in a temporary dip in performance.

Parameters: Image Data

The input_shape refers to the dimensions of an image: height, width, and color (RGB) values. image_size comes from the input_shape.

Note, one issue I had early on with image_size. I tried non-square images (which hurt training and aren’t recommended for CNNs) and found the hard way most of the image parameters for width and height reverse their order.

For example, this is what’s needed:

...
    val_dir,
    target_size = (height_here, width_here),
...

I was expecting:

...
    val_dir,
    target_size = (width_here, height_here),
...

It bit me hard, as most frameworks I’ve used expect width first and then height. I mean, even when we talk screen resolution we list width then height (e.g., 1920x1080). Just be aware when using rectangle images. Always RTFM (because, apparently, I didn’t).

The train_test_ratio controls how many images are held back for testing the model. I’d have to run through the code again, but I don’t think this is needed. As the preprocessing script created a folder with validation images. Hmm, I’ll add it to my tech debt list.

The zoom_range parameter controls how far the script should zoom in on the images. And, lastly, shear_range controls how much of the images to clip from the edges before feeding them to the CNN.

Parameters: CNN Hyperparameters

A “hyperparameter” is what machine-learning engineers call parameters which may impact the outcome of training a neural-net.

Here are the hyperparamters we’ve exposed:

batch_size refers to the number of photos a neural-net should attempt predictions on before updating the weights of each perceptron. Note, the highest batch size is usually limited by your GPU RAM. Locally, I use a GTX 1060 with 6GB of RAM–I couldn’t get a batch bigger than around 16. YMMV.

steps_per_epoch are the number of batches to go through before considering one epoch complete. An epoch is an arbitrary number representing how many batches * steps_per_epoch to go through before considering the training complete.

So, the length of training would be training schedule = epochs * steps_per_epoch * batch_size

validation_steps is the number of batches from the training data to use for validating the current weights. This will be used when we fit (train) our classifier and when we evaluate it.

optimizer is the name of the optimizer used. This is the heart of training, as it is responsible for deciding how the the weights should be updated after each batch.

I’ve setup the code to only use one of three optimizers, either adam, adagrad, sgd.

def get_optimizer(optimizer, learning_rate = 0.001):
    if optimizer == 'adam':
        return tensorflow.keras.optimizers.Adam(lr = learning_rate, beta_1 = 0.9, beta_2 = 0.999, epsilon = None, decay = 0., amsgrad = False)
    elif optimizer == 'sgd':
        return tensorflow.keras.optimizers.SGD(lr = learning_rate, momentum = 0.99) 
    elif optimizer == 'adadelta':
        return tensorflow.keras.optimizers.Adadelta(lr=learning_rate, rho=0.95, epsilon=None, decay=0.0)

Here is more information on optimizers.

Easy to read:

Primary source:

The primary reason, as I understand it, to use adagrad over adam, is adagrad’s learning_rate will naturally modify itself to be more conducive to optimal convergence.

However, there are many optimizers. A lot of them available in Keras:

  • Stochastic Gradient Descent (SGD)
  • RMSprop
  • Adagrad
  • Adadelta
  • Adam
  • Nadam
  • Adamax

Keras’ docs on optimizers:

The learning_rate controls how drastically the optimizer should change the perceptrons’s weights when they have made an incorrect prediction. Too high, it won’t converge (learn) too low and it will take a while.

You will find a lot of documentation saying, “The default learning rate of an optimizer is best, it doesn’t need to be changed.” I’ve found this advice to be true, well, mostly. I did run into an issue when using adam’s default setting of 0.001 in this project. The neural-net just didn’t learn–I had to drop it to around 0.0001, which did much better.

A starter read on learning rate:

It’s not exhaustive. If you interested in tweaking the optimizer or learning rate, Google and read as much as possible.

Lastly, val_save_step_num controls how many training epochs should pass before the validator tests whether your model is performing well on the test set. We have the code setup such if the validator says the model is performing better than any of the previous tests within this training session, then it will save the model automatically.

Classifier Code: Data Preparation

The make_dir allows making a directory, if it doesn’t already exist. We then use it to create our model save directory.

def make_dir(dir_path):
    if not os.path.exists(dir_path):
        os.mkdir(dir_path)

# Create needed dirs
make_dir(model_save_dir)

The next bit saves the classes the train_gen found to a file. This is useful later when we are trying to quickly deploy the model to production.

# Save Class IDs
classes_json = train_gen.class_indices
num_classes = len(train_gen.class_indices)

This saves one object to a json file. The key (e.g., “2456”) represents the code provided by LEGO. And the value is the numeric class assigned by the classifier.

{
    "2456": 0,
    "3001": 1,
    "3002": 2,
    "3003": 3,
    "3004": 4,
    "3010": 5,
    "3039": 6,
    "32064": 7,
    "3660": 8,
    "3701": 9
}

We can do the following after we’ve trained the model:

predicted_lego_code = json_classes[model.predict()]

And the model will return the LEGO class it has identified.

Classifier Code: Data Generator

When dealing with CNNs, often, the training data are too large to fit in RAM, let alone GPU RAM, at once.

Instead, a DataGenarator is used. A DataGenerator is class provided by Keras, it loads training data in manageable chunks to feed to your model during training. Let’s run through using it.

We initialize ImageDataGenerator – a subclass of kerasDataGenerator. Then, we create two flows, one for loading data from the training folder into the model. The other is the same, however, it loads data from the test folder. The latter will be used to validate the model.

Parameters used in our ImageDataGenerator:

  • shear_range – this controls how much of the images’ edge is trimmed off as a percentage of the whole image. This is useful for quickly reducing the size of images (thereby increasing training speed).
  • zoom_range – is how far to zoom in before feeding the image to the model.
  • horizontal_flip – if this is set to true, the images are randomly mirrored horizontally. This essentially doubles your training images. Though, it shouldn’t be used in all cases. If the target has a “handediness” to it, then this would destroy accuracy. A simple example of this downfall would be training a CNN to determine whether baseball player is left or right handed.
  • validation_split – determines the percentage of images held back for validation.
# These Keras generators will pull files from disk
# and prepare them for training and validation.
augs_gen = ImageDataGenerator (
    shear_range = shear_range,  
    zoom_range = shear_range,        
    horizontal_flip = True,
    validation_split = train_test_ratio
)  

Now,the parameters of the ImageDataGenerator.flow_from_directory methods:

  • target_size – this one bit me. It’s the size of your images as a tuple (e.g., “(150, 150)”). It expects height then width.
  • batch_size – this is the number of images loaded into the GPU RAM and trained on before updating the weights.
  • class_modean import argument. This sets up the targets for the model’s attempt at prediction. sparse indicates the targets will be LabelEncoded.

Below lies a tale of woe I keep hinting at.

If you have more than one class to predict, like us, you have two options. Either sparse or categorical.

Sparse

target
1
2
3
2

Categorical

1 2 3
1 0 0
0 1 0
0 0 1
0 1 0

However, this is where the bug in the original code was. It had setup the targets as categorical, however, it used binary_crossentropy as the loss function. This error is difficult to catch–it’s the machine-learning equivalent of the “there” and “their” error.

With the mismatch of targets and loss function there’s no help either. The model will still compile and train without problems. But the cruel combination of categorical targets and binary_crossentropy leads to an extremely high accuracy but an extremely bad production accuracy. The problem is the loss function is only looking at column 1 in the categorical table above. If the model model predicts it is 1 when the first column is 1 then it thinks its “correct.” Otherwise, if the model predicts a 0 when column 1 is 0, then the model still thinks its correct. After all, “it wasn’t 1.” And to be clear, the model isn’t wrong–we’ve just given it the wrong target labels.

This is the quintessential “hotdog, not a hotdog” problem.

In short, if you feel your model quickly trains to an accuracy too good to be true, it is.

train_gen = augs_gen.flow_from_directory (
    train_dir,
    target_size = image_size, # THIS IS HEIGHT, WIDTH
    batch_size = batch_size,
    class_mode = 'sparse',
    shuffle = True
)

test_gen = augs_gen.flow_from_directory (
    val_dir,
    target_size = image_size,
    batch_size = batch_size,
    class_mode = 'sparse',
    shuffle = False
)

Classifier Code: Building the Model

Close to done. I’m not going to go over the design of a CNN for two reasons. I’m still learning what it all means and there are much better explanations elsewhere.

However, there are a couple of things important to us.

  • num_classes is the number of LEGOs we are trying to classify.
  • activation on the last layer controls the type of output from the CNN. It will need to correspond with the optimizer and will need to correspond to the class_mode setting of the the DataGenerators.
  • build_model is a convenience function. It allows us to quickly build a Keras CNN model and return it to be used.
  • model.summary outputs a text diagram of the model.
  • model.compile prepares the entire model for training.
def build_model(opt, input_shape, num_classes):
    model = tf.keras.models.Sequential()
    model.add(tf.keras.layers.Conv2D(32, (3, 3), input_shape = input_shape))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.Dropout(0.2))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    
    model.add(tf.keras.layers.Conv2D(64, (3, 3)))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.Dropout(0.2))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    
    model.add(tf.keras.layers.Conv2D(128, (3, 3)))
    model.add(tf.keras.layers.Activation('relu'))
    model.add(tf.keras.layers.Dropout(0.2))
    model.add(tf.keras.layers.MaxPooling2D(pool_size=(2, 2)))
    
    model.add(tf.keras.layers.Flatten())  # this converts our 3D feature maps to 1D feature vectors
    model.add(tf.keras.layers.Dense(256))
    model.add(tf.keras.layers.Activation('relu'))
    
    model.add(tf.keras.layers.Dropout(0.2))
    
    model.add(tf.keras.layers.Dense(num_classes, activation = 'softmax'))
    return model

#################################
# Create model
#################################

selected_optimizer = get_optimizer(optimizer, learning_rate)

model = build_model(selected_optimizer, input_shape, num_classes)
model.summary()

model.compile(
    loss = 'sparse_categorical_crossentropy',
    optimizer = selected_optimizer,
    metrics = ['accuracy']
)

Classifier Code: Creating Callbacks

Before we execute training we should setup of Keras callbacks.

These pre-written callback functions will be passed to the model and executed at important points throughout the training session.

  • ModelCheckpoint this method is called after the number of epochs set by val_save_step_num. It runs a validation batch and compares the val_loss against other past scores. If it is the best val_loss yet, the method will save the model and, more importantly, weights to the best_model_weights path.
  • TensorBoard opens a TensorBoard session for visualizing the training session.
best_model_weights = model_save_dir + 'base.model'

checkpoint = ModelCheckpoint(
    best_model_weights,
    monitor = 'val_loss',
    verbose = 1,
    save_best_only = True,
    mode = 'min',
    save_weights_only = False,
    period = val_save_step_num
)

tensorboard = TensorBoard(
    log_dir = model_save_dir + '/logs',
    histogram_freq=0,
    batch_size=16,
    write_graph=True,
    write_grads=True,
    write_images=False,
)

Before any KerasCallbacks can be added to the training session, they must be gathered into a list, as it is how training method will except to receive the

callbacks = [checkpoint, tensorboard]

Classifier Code: Training

Gross, I need to rewrite this portion of the code. It is a kludge way to restart a training session after interruption.

It checks if you indicated you want to continue a session. It then loads the best saved model and evaluates it on the test data.

if continue_training:
    model.load_weights(best_model_weights)
    model_score = model.evaluate_generator(test_gen, steps = validation_steps)

    print('Model Test Loss:', model_score[0])
    print('Model Test Accuracy:', model_score[1])

And here, we come to the end. The following function executes the training session. It will initialize the callbacks, then train for the number of epochs set. Each epoch it is pulling a batch of data from the train_gen (DataGenerator), attempting predictions, and then updating weights based on outcomes. After the number of epochs set in the checkpoint callback, the model will pull data from the test_gen, these data it has “never” seen before, and attempt predictions. If the outcome of the test is better than the outcome of any previous test, the model will save.

history = model.fit_generator(
    train_gen, 
    steps_per_epoch  = steps_per_epoch, 
    validation_data  = test_gen,
    validation_steps = validation_steps,
    epochs = epochs, 
    verbose = 1,
    callbacks = callbacks
)

Whew, that’s it. The above model converged for me after 20 minutes to 98% validation accuracy. However, there’s lots left to do though. As I’ve said before, “Just because we have high validation accuracy does not mean we will have high production accuracy.” In the future, I’ll be writing about the turntable for quickly generating training data. It’s nifty. Based on a NEMA17, RAMPS kit, and RPi with RPi Camera. It’s the bomb-dot-com.

A LEGO Classifier -- CNN and Elbow Grease

I’ve a robot friend. To be clear, the friend is not a robot, rather, we build robots together. One of the projects we tossed about is building a LEGO sorting machine. Rockets is the friends name–again, not a robot–teaches robotics to kids. For their designs, LEGOs are the primary component. Unfortunately, this results in much time spent to preparing for an event.

He mentioned to me, “What I really need is a sorting machine.” And proceeded to explain his plain for building one.

I was skeptical for some time, but finally, I got drawn in he talked about incorporating a deep neural-network. More specifically, a convolutional neural-network (CNN). I’d been looking for an excuse to build a CNN. This was a good one.

Anyway, these blog posts are our journal in build the LEGO sorter.

Before we get started, a note about this series: I won’t spend much time on explaining parts of the work where it is better documented elsewhere. Instead, I’m going to focus on stuff I’ve found everyone else omitting. Like, putting the neural-network to work. This one bugged me. Everyone loves to say, “Dude, my classifier has a validation accuracy of 99.999%!” That’s great, but as we found out, validation accuracy doesn’t always translate into production accuracy.

TL;DR

If you don’t want to listen to my rambling or want to do things the easy way, you can jump straight into the code using Google’s Colab:

This notebook is setup to download Rocket’s data and train the classifier. Thanks to Google for providing a GPU to train on and Github for hosting the data.

Or if you want to run the code locally, Rocket made the training data public. Just know, you’ll need a GPU.

Then jump to the code by clicking here.

The Idea

It was pretty straightfoward to begin with. We’d find some images of LEGOs on the internet and then train a CNN to classify them by their part code. It was a bit naive, but that’s where must projects being, right? Hopeful naiveté.

Anyway, we searched the webs for projects like this, as we hoped they had prepared images. Google told us several folks doing similar work. I’m not going to list them all, only what I considered worth a read:

This is an extremely well documented project by Paco Garcia.

So, after reading a few articles, we figured we could do this. We just needed data. After a bit more searching we found the following datasets:

I wasn’t happy about these datasets. Their structures weren’t great and they were not designed to help train a classifier. But then, Rockets found Paco had actually opened his dataset to the public:

One bit more, Paco also made his code public:

Paco, you are a robot friend, too!

Alright, we were encouraged by Paco. We knew the project would be possible. However, we didn’t want to step on brownfield. We needed the green. Or if you don’t speak dev, we didn’t want to do this the easy way and replicate Paco’s work. We wanted to really beat ourselves up by doing everything from scratch.

Creating a Dataset

As I stated before, I didn’t like any datasets but Paco’s. It was real images and meant to train a classifier. But, they weren’t the LEGOs we wanted to classify. Rockets’s LEGO projects involve a lot of technic bricks, which didn’t seem to be in Paco’s mix. So, we set out to create our own.

The first attempt creating training images was by rendering images from .stl files found on the internet using the Python version of Visualization Toolkit. I won’t cover it here since it was a fail and as I’ll create an article later about the stuff we tried and didn’t work.

Anyway, while I was working on it Rockets had a brilliant plan. He created an instrument to take pictures of a LEGO on a spin plate. It used a Raspberry Pi, Pi Cam, and stepper motor, and unicorn farts.

Then Rockets began taking pictures of 10 classes of LEGOs. Not sure how long this took , but shortly he pinged me saying he had 19,000 images. (Ok, ok, he might be part robot.)

I’m not going to attempt explaining the build, as I believe Rockets will do this later. Besides, about the only part I understand is the unicorn flatulence.

Alright! Now I needed to get my butt in gear and fix up the software.

Preprocessing Code

Before we could start training a CNN on Rockets’s images we needed to do some preprocessing. First, the images came in at full resolution, but we needed to crop them, as the CNN train better on square image. Of course, the image would need to be cropped as not to lose the target data (the LEGO).

For example preprocess-image-for-cnn

Also, the trainer would be expecting a file structure something like this:

data
├── test
│   ├── 2456
│   │     └── 2456_0001.jpg
│   │     └── 2456_0002.jpg
│   │     └── 2456_0003.jpg
│   │     └── ....
│   ├── 3001
│   ├── 3002
│   ├── 3003
│   ├── 3004
│   ├── 3010
│   ├── 3039
│   ├── 32064
│   ├── 3660
│   └── 3701
└── train
    ├── 2456
    ├── 3001
    ├── 3002
    ├── 3003
    ├── 3004
    ├── 3010
    ├── 3039
    ├── 32064
    ├── 3660
    └── 3701

Therefore, I’ve written a Python script to do the following

  1. Take a path where images are stored by name of the class
  2. Load the image
  3. Resize the image to specified size
  4. Crop from the center of the image out
  5. Create a train and test folder
  6. Create sub-folders in train and test with the class name
  7. Shuffle the images in the process
  8. Save the cropped file in the appropriate folder, depending what percentage of images you want to withhold for testing.
  9. Repeat steps 2-8 for every image

Let’s jump into the code.

The full code can found here:

But I’ll walk through the code below.

Preprocessing Code: Needed Libraries

import os
import glob
import cv2
import random

The only non-standard Python library we are using is:

This may be a bit tricky depending on which OS you are using and whether you are using Anaconda or straight Python. However, the following is what we used:

pip install https://pypi.org/project/opencv-python/

If you have any troubles load the cv2 library, it probably means there was an issue installing OpenCV. Just let me know in the comments and I can help debug.

Preprocessing Code: Processing Parameters

The following control the the flow of preprocessing

  • dry_run: if set to true, it does not save the images, but does everything else
  • gray_scale: converts the images to gray-scale.
  • root_path: the root folder of the project
  • show_image: shows the before and after of the image.
  • output_img_size: adjust this to the size of your desired output image
  • grab_area: the total area of the original image to take before resizing
  • train_test_split: the rate of test images to withhold
  • shuffle_split: should the images be shuffled in the process
  • part_numbers: a list of all the class folders contained in the input
#####################
# Parameters
#####################     

dry_run                 = False # If true, will print output directory.
gray_scale              = True

root_path               = './data/'
input_path              = f'{root_path}raw/size_1080/'
output_path             = f'{root_path}cropped/'

show_image              = False

output_img_size         = (300, 300)
grab_area               = 500
train_test_split        = 0.3
shuffle_split           = True

part_numbers            = [
                           '2456',
                           '3001',
                           '3002',
                           '3003',
                           '3004',
                           '3010',
                           '3039',
                           '3660',
                           '3701',
                           '32064'
                        ]

Below is the main loop. It is going to repeat for every folder it finds in the the root folder.

for part_number in part_numbers:

    part_input_path  = f'{input_path}{part_number}/'
    
    # Get input file paths.
    image_files = glob.glob(f'{part_input_path}*.jpg')
    num_files = len(image_files)

    # Image index.
    index = 0

    # If true, the images will be loaded and then split at random.
    if shuffle_split:
        file_index = random.sample(range(1, num_files), num_files - 1)
    else:
        file_index = range(1, num_files)

This is the inner loop, it loads each of the image files in the class class folder, modifies it, and saves it to the output folders.

    for file_num in file_index:
        
        # Increment the file index.
        index += 1
        
        # Load the image
        input_file_path = f'{input_path}{part_number}/{str(file_num).zfill(4)}.jpg'
        print(f'LOADED: {input_file_path}')
        
        # Crop raw image from center.
        img = cv2.imread(input_file_path)

        # Get the center of the image.
        c_x, c_y = int(img.shape[0] / 2), int(img.shape[1] / 2)
        img = img[c_y - grab_area: c_y + grab_area, c_x - grab_area: c_x + grab_area]
         
        # Resize image
        img = cv2.resize(img, output_img_size, interpolation = cv2.INTER_AREA)
        
        # Should we convert it to grayscale?
        if gray_scale:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        
        # Show to user.
        if show_image:
            cv2.imshow('image', img)
            cv2.waitKey(0)
            cv2.destroyAllWindows() 

        # Determine if it should be output to train or test.
        test_or_train = 'train'        
        if index < int(num_files * train_test_split): 
            test_or_train = 'test'
        
        # Prepare the output folder.
        color = ''
        if gray_scale:
            part_output_folder = f'{output_path}gray_scale/{test_or_train}/{part_number}/'
        else:
            part_output_folder = f'{output_path}color/{test_or_train}/{part_number}/'
            
        # Make the output directory, if it doesn't exist.
        if not os.path.exists(part_output_folder):
            os.makedirs(part_output_folder)

        # Create part path.
        part_image_path = f'{part_output_folder}{part_number}_{index}.jpg'
        
        # Output
        if dry_run:
            print(f'Would have saved to: {part_image_path}')
        else:
            print(f'SAVED: {part_image_path}')
            cv2.imwrite(part_image_path, img)

Fairly straightfoward. Just make sure to run to run the script from the main directory. For example

project_folder
└── square_crop.py <--- run from here
└── data
    ├── test
    │   ├── 2456
    │   │     └── 2456_0001.jpg
...

Or, if you don’t want to do it the hardway. Rockets has made his images available

Next

Next, I’m going to dive into the Tensorflow CNN code. Stay tuned, my robot friends!

Setup a Local MySQL Database

The last two articles have been getting oriented to SQL, however, the information in them will disappear quickly if we don’t give you a way to practice on data meaningful to you. Let’s face it, as much fun as it is to find out random employees salaries, those don’t mean anything to you.

This article will show you how to setup a copy of MySQL Server on your PC, connect to it, load data from a CSV, and query those data. There’s a lot to get done, so let’s get started.

Local MySQL Server Setup

Each of the three operating systems are a little different on how you must go about setting up a local copy of MySQL Server. Unfortunately, Windows is the most complex. Anyway, feel free to skip to the appropriate section

Windows

First, download the MySQL MSI Installer.

mysql-windows-installer-download

After you’ve downloaded it, open the the file.

mysql-windows-installer-download

If you are prompted to “Upgrade” go ahead and say “Yes”

mysql-windows-installer-download

The installer is a bit confusing, but don’t worry, most everything is fine left on its default.

Click on MySQL Server then the Add button. Add “MySQL Server” and “Connector/OBDC x64.” Then click “Next.” You will see a Installation summary, click on “Execute” and wait for the download to finish and then install wizard to begin.

As I stated, most of the install wizard questions we will leave as default.

mysql-windows-installer-download

mysql-windows-installer-download

mysql-windows-installer-download

mysql-windows-installer-download

On the “Accounts and Roles” section you will need to decide on your password for the SQL Server on your local PC. I obviously didn’t pick a great one. MySQL Server will automatically setup a user with the name of root and a password you set here. This root user will permissions to do anything to the server.

mysql-windows-installer-download

mysql-windows-installer-download

Execute the installer and let it finish. mysql-windows-installer-download

Once it finishes you should now have MySQL Server installed on your local PC. Skip to the last section to test it out.

Mac

Mac’s a bit simpler.

Download the .dmg installer. mysql-windows-installer-download

Click on “No thanks, just start my download” and when the download is finished, double click on it. mysql-windows-installer-download

Double click on the installer. You will need to enter your system password for the installer to setup MySQL Server, but you will also need to provide the MySQL Server root user a password. Don’t consfuse the two, unless you plan for them to be the same.

mysql-windows-installer-download

Once it finishes you should now have MySQL Server installed on your Mac. Skip to the last section to test it out.

Testing your Local SQL Server

Go ahead and open MySQL Workbench and let’s connect to this new local server.

Click on the “New Connection” icon and leave everything default, except the “Connection Name,” here enter localhost. mysql-windows-installer-download

Double click on the new connection and enter the password you created during installation. Voila!

Let’s run a command to make sure everything is working.

SHOW databases;

You should see: mysql-windows-installer-download

Loading CSV

Create a database

CREATE DATABASE name_of_your_database;

Before we create a table, make sure we are using the created datebase.

USE name_of_your_database

Now, we need to go over a bit of boring stuff before we get to loading the CSV. Sorry, I’ll try to keep it brief.

Datatypes

In SQL, every field has something called a “datatype.” You can think of a datatype as a tag on your data tell the computer how to read them.

Ultimately, a computer can’t make sense of any human-words. It has to convert everything into 0 and 1 before it understand its. If this conversion was left up to the computer entirely, it might see a word and say, “Oh, yah, this is one of those French words,” when it is actually English, thus, the conversion to 0 and 1s are incorrect.

You may have encountered this in a spreadsheet. If you open a spreadsheet and see something like

xkcd-types

The data I actually provided the spreadsheet were:

Zipcode
75444
06579

Notice the zero in front of 6579, this was due to the computer saying, “Oh, these data look like numbers–and since the human didn’t tell me otherwise, I’m going to treat them like numbers. And, well, it is perfectly valid to drop the leading zero of a number.”

I wish all datatypes were this simple, however, the above example is about is simple as it gets. We can try to skip over a lot of nuances of datatypes and focus on the three we will probably see the most:

  • DATE
  • TIME
  • INT (short of integer)
  • FLOAT
  • CHAR (short for character)

Here are what samples of the above data would look like in a spreadsheet:

DATE TIME INT FLOAT CHAR
2019-10-01 2019-10-01 12:01:22 42 42.4 The answer to it all.

DATE

Dates are pretty straightforward, they store a year, month, and day as a number. However, when we retrieve this number it is put in the human readable format listed above.

TIME

Time is exactly like DATE, but it also includes hours, minutes, and seconds (sometimes milliseconds).

INT

An INT stores a number no bigger than 2,147,483,647. However, one thing an INT cannot do is store a partial numbers. For example, if we try to store 0.5 in an INT field it will probably get converted to 1.

FLOAT

FLOATs fill in where INTS fail. That is, a FLOAT store only up to the precision you specifiy. For example, if we tried to store a 0.5 in a FLOAT with two precision points we’d be fine. However, if we tried to store 0.4567 in a FLOAT with only two precision points, then it would be converted to 0.46, or rounded up.

CHAR

CHAR is meant to store human readable text. When you put data into a CHAR field, the SQL program knows this is human readable information and doesn’t try to figure it out at all. It leaves it literally as it is. This is why CHARS are known as “literals.” They are also called “strings,” because the computer seems them as a bunch of characters strung together.

SQL Datatypes

In SQL there are a lot of datatypes, however, some you may never need to use. One way SQL is a bit different than a spreadsheet is it wants to know ahead of time the size it needs to make the field.

CHAR Revisited

This will mainly impact us when dealing with CHAR. When the SQL program creates a CHAR field it wants to know the maximum number of characters which will ever go into the field.

For example:

  • CHAR(19) could hold the following: <-------19-------->
  • CHAR(5) could hold the following: <-5->

One important note, if you put a single character in a CHAR(5) field, then the SQL program will fill in the other four characters with a NULL. In short, a CHAR field will always be full.

VARCHAR

There is another type of character field which allows you to put more or less data than was decided at the beginning. The VARCHAR datatype stands for “variable character” field. It will allow you to store up to 65,535 characters on MySQL. This is around 3 pages of text.

VARCHAR vs. CHAR

Why have CHAR at all? Shouldn’t we always use VARCHAR for everything just in case? Well, usually, but not always.

Often, when you design a database you want to make it as efficient as possible (I mean, it’s going to be successful business product, right?). The maximum size of the data a human will try to stuff in the field is important to the SQL program, as it tries to store data in such a way it minimizes space used and maximizes efficiency in retrieving the data.

In short, CHAR has a few advantages for your database. And take Social Security Numbers, if your database has to store these data then it should probably be a CHAR as these data have historically been 9 characters (11 if you include dashes).

Pop quiz, why don’t we store a Social Security Number as an INT?

Creating the Table

Ok, I’ve put you through a crash course of datatypes to get you to this point.

We are going to:

  1. Create a database called tasksDB
  2. Active tasksDB
  3. Create a table on tasksDB, setting the fields datatype
  4. Then import a CSV into this table
  5. Lastly, we will write a query against the table

Ready!? Let’s do it!

Creating Database

Open Workbench, type, and run the following:

CREATE DATABASE tasksDB;

SHOW databases;

USE tasksDB;

SELECT * FROM tasks;

LOAD DATA INFILE './task.csv'  INTO TABLE tasks
FIELDS TERMINATED BY ','
ENCLOSED BY '"' 
LINES TERMINATED BY '\n';

SELECT * FROM tasks;

CREATE TABLE IF NOT EXISTS tasks (
    task_id INT AUTO_INCREMENT,
    title VARCHAR(255) NOT NULL,
    start_date DATE,
    due_date DATE,
    status TINYINT NOT NULL,
    priority TINYINT NOT NULL,
    description TEXT,
    PRIMARY KEY (task_id)
)  ENGINE=INNODB;

https://superuser.com/questions/1354368/mysql-error-in-loading-csv-file-data-into-table

Understanding the MySQL Query

Welcome back! Alright, now we know how to connect to a remote server from within MySQL Workbench, let’s start writing some queries.

Here’s a common SQL query:

    SELECT e.emp_no,
		   e.last_name,
           t.title
      FROM employees AS e
 LEFT JOIN titles 	 AS t
        ON e.emp_no = t.emp_no
     WHERE e.hire_date > '1999-12-31'
  ORDER BY e.last_name DESC;

This query produces the following table when run on our employees database.

row_num emp_no last_name title
0 47291 Flexer Staff
1 60134 Rathonyi Staff
2 72329 Luit Staff
3 108201 Boreale Senior Engineer
4 205048 Alblas Senior Staff
5 222965 Perko Senior Staff
6 226633 Benzmuller Staff
7 227544 Demeyer Senior Staff
8 422990 Verspoor Engineer
9 424445 Boreale Engineer
10 428377 Gerlach Engineer
11 463807 Covnot Engineer
12 499553 Delgrande Engineer

When the data are presented like this, it appear similar to our traditional Excel spreadsheet, right?

Let’s compare the SQL query and a spreadsheet.

compare-excel-and-sql

Now, here in a few weeks when you are SQL-writing-machine you’ll notice this analogy between Excel and a SQL query breaks down. But for now, let the above image comfort you in knowing the core functions of SQL are similar to those of a spreadsheet. And you know these spreadsheet functions well.

  • Selecting columns
  • Filtering columns and rows
  • Ordering rows
  • Combining data sets

However, SQL has a lot of superpowers an Excel spreadsheets doesn’t. Of course, the tradeoff is you must leave behind the comfort of a graphical user interface. But don’t let it scare you off–it only takes a 3-4 months to get used to, but then you’ll realize how much those graphical interfaces have been chaining you down.

Alright, back to the queries. Let’s take a look at the different parts of the query above.

SELECT

The SELECT statement is how you choose what turns up in the results section. If don’t put something in the SELECT area, then you will not get anything. It is often used to retrieve data, called fields, from one or more tables within a database.

Select Area

You may ask, what is the “SELECT area.” It is everything between the word SELECT until FROM.

SELECT -------------------------------
       -- ALL THIS IS THE SELECT AREA
       -------------------------------
FROM

Select Fields

There are two different ways to SELECT fields you want to get results from. You can use the *, which means “everything.” Or you can list the field names you want returned. Each item you put in the SELECT area should be followed by a comma, unless it is the last item.

For example:

    SELECT  emp_no,
            last_name,
            title
...

The code above requests three different fields be returned in the result set: emp_no, last_name, and title.

Or

    SELECT  *
...

Returns every field, in every table listed.

I should point out, if you forget a comma it can get messy. Often, the SQL server will send an error message, but not always. As we will see in a moment.

Select Calculations

The SELECT does more than retrieve data from tables within a database. It can also perform on-the-fly calculations, such as

SELECT 1 + 1,
       2 *25,
       55 / 75,

This should return the following:

  1 + 1 2 *25 55 / 75
0 2 50 0.7333

FIELD

A field in SQL is similar to the column in a spreadsheet. It contains data of the same type on every row (more on datatypes later). Fields may be referenced throughout a SQL query, but for them to show in the query results they must be included in the SELECT area–as we went over in the “SELECT” section above.

SELECT emp_no,
       first_name,
       last_name
FROM employees

Ambiguous Field List

The above query works. However, try running the following query, which includes two tables.

SELECT emp_no,
       first_name,
       last_name
FROM employees
LEFT JOIN titles
    ON employees.emp_no = titles.emp_no

You get any results? Me either. Only an error message from the database stating:

Error Code: 1052. Column 'emp_no' in field list is ambiguous	

This is because both the employees and titles table have a field named emp_no and the SQL program can’t figure out which you want.

To solve this, we add the table name plus . to the front of each field name. This will tell the SQL program from which tables we would like to field to come from–leaving no ambiguity. Computers hate ambiguity.

Let’s run the query again with table names.

SELECT employees.emp_no,
       employees.first_name,
       employees.last_name
FROM employees
LEFT JOIN titles
    ON employees.emp_no = titles.emp_no

This time we get the results we expected, without error.

Building on this, a good SQL coder will always prepend the table name to the front of the query, whether it’s required or not. This prevents future mistakes.

For example, let’s say you wrote this code:

SELECT emp_no,
       salary
FROM salaries

And your code was put into production (a term meaning put to use by your business) then a year later another coder added a second table to the query without critically looking at the query as a whole (something a bad SQL coder forgets to do).

The new query looks like this:

SELECT emp_no,
       salary,
       first_name,
       last_name

FROM salaries
LEFT JOIN employees
    ON salaries.emp_no = employees.emp_no;

Try to run this query. You will find the same field list is ambiguous error as we saw earlier.

The deeper lesson here is: A good coder is like a defensive driver; they code in a way it expects others to be reckless.

Back to the example above, if we include the table in the field names, then it doesn’t matter if a reckless coworker adds another table.

SELECT salaries.emp_no,
       salaries.salary,
       employees.first_name,
       employees.last_name

FROM salaries
LEFT JOIN employees
    ON salaries.emp_no = employees.emp_no;

Field Aliases

Often you will want to export your results into a CSV to send to someone. You may have noticed when you execute a query SQL returns the results in a neat spreadsheet. I don’t know if I’ve mentioned it, but you can export these results in a CSV by hitting the little disk button above the results.

mysql-workbench-export-to-csv

However, you may not like the machine formatted column names. I mean, don’t get us wrong, we’re nerds! We read machine friendly words fine, but our bosses don’t.

Well, MySQL has a built in command allowing you to rename fields (and more) on the fly. This command is AS and is seen in the query below written to rename the column names.

SELECT salaries.emp_no 		AS Id,
       salaries.salary		AS Salary,
       employees.first_name	AS "First Name",
       employees.last_name	AS "Last Name"

FROM salaries
LEFT JOIN employees
    ON salaries.emp_no = employees.emp_no;

Now the column headers have “boss-friendly” names.

mysql-workbench-export-to-csv

You’ve probably noticed the first two aliases are written without quotation marks and the second two are surrounded by them. The SQL program can get confused by spaces, so we wrap the new name in " marks. When the SQL program sees these marks, it says to itself, “Oh, I bet the user is going to have one of those fancy human names, I’m going to assume everything between the first quotation mark and the next one I find is all one fancy human word. Silly humans.”

A more technical term for someone inside quotations marks is a literal constant. However, programmers know them as “strings.” It’s probably b

Don’t Lose Your AS

Go ahead and try to run this query:

SELECT emp_no
	   first_name,
       employees.last_name
FROM employees;

Did you run it? Anything jump out as weird? You don’t really run it did you? Go run it, I’ll wait.

Ok, you’ll see something like this: | first_name | last_name | |:———–|:———-| | 10001 | Facello | | 10002 | Simmel | | 10003 | Bamford | | … | … | Super weird right? There are only two columns and it seems like the column names are jumbled up. That’s exactly what’s happened. It’s due to a missing , right after the emp_no. This is a result of something in SQL I think is silly–you can omit the AS keyword between a field and its alias.

Meaning, we could rewrite the query from earlier where we showed alias use like this:

SELECT salaries.emp_no 		Id,
       salaries.salary		Salary,
       employees.first_name	"First Name",
       employees.last_name	"Last Name"

FROM salaries
LEFT JOIN employees
    ON salaries.emp_no = employees.emp_no;

But, the first time you miss a comma you’ll be asking, “Why!? Why does MySQL allow this!” I’m not sure, but we have to deal with it. This is why I ask you always include the AS keyword. Again, you are helping prevent bugs before they happen.

FROM

As you’ve already seen, the FROM command tells SQL where on the database it should look for data. If you don’t specify a table in the FROM clause, then the SQL program acts if it doesn’t exist, and will not be able to find the fields you request.

SELECT *
FROM employees
LEFT JOIN departments
    ON employees.emp_no = departments.emp_no

In the next article we are going to talk about JOINS, they are an extension to the FROM clause of a query, but, they deserve their own article. Right now, look at the LEFT JOIN as an extension of the FROM clause. A join tells the SQL program, “First look in the employees table, then, check in the departments table, if there is a relationship with the employees table.”

Like I said, we will review JOINS thoroughly in the next article.

Table Aliases

Like we could give fields nicknames, called aliases, we can do the same with table names. However, this is usually done for a different reason: To save on typing.

One of the primary reason bad coders don’t write out the table names (not you, you’re going to be a good coder) is it adds a lot more to type. You may say, “Well, that’s just lazy.” It is, but it’s smart-lazy–also know as efficient. And efficiency is something you want to strive for in your code and coding.

Let’s look at an example from earlier.

SELECT salaries.emp_no 		AS Id,
       salaries.salary		AS Salary,
       employees.first_name	AS "First Name",
       employees.last_name	AS "Last Name"

FROM salaries
LEFT JOIN employees
    ON salaries.emp_no = employees.emp_no;

This query could be rewritten by using table aliases and save a lot of typing. It’s probably best to show you.

SELECT s.emp_no 	AS Id,
       s.salary		AS Salary,
       e.first_name	AS "First Name",
       e.last_name	AS "Last Name"

FROM salaries       AS s
LEFT JOIN employees AS e
    ON s.emp_no = e.emp_no;

Execute this query and compare its results to the query without table aliases. You will find the results are exactly the same. Moreover, this rewrite has saved 45 keystrokes. You may think, “Eh, not much.” Well, this is a small query. Imagine writing queries twice this size all day long. Your savings are worth it–may the time for an extra cup of coffee (or pot, in my case).

It is also easier for the human brain to comprehend–at least, once you’ve been reading SQL for awhile. Your brain will understand e and employees the same, but it doesn’t have to work as hard to understand e.

In short, good coders use table aliases.

ORDER BY

In spreadsheets there will usually be a way to sort your data. Often your options will be based on a column’s contextual order. If the data are numbers it will be low-to-high, or high-to-low, respectively. If it’s text then your choice will probably be alphabetical, either A-Z to Z-A. And if it’s a date, then it will be first-to-last, or last-to-first. Each of these order types share a commonality, they value either goes to a low-values to high-values, or high-values to low-values. These types of ordering are known as ascending and descending, respectively.

In SQL, there are two types of ORDER BYs, ASC and DESC, for ascending and descending. They operate a bit different than most spreadsheet applications. They still order data by low-to-high or high-to-low, however, when you apply an ORDER BY it affects the entire result set. When a field is targeted by an ORDER BY all other fields on the same row are ordered along with the targeted field.

Enough words. Let’s take a look at some examples:

DESC

SELECT employees.emp_no,
       employees.first_name,
       employees.last_name
FROM employees
ORDER BY employees.emp_no DESC

mysql-workbench-export-to-csv

ASC

SELECT employees.emp_no,
       employees.first_name,
       employees.last_name
FROM employees
ORDER BY employees.emp_no ASC

mysql-workbench-export-to-csv

One note about ASC, if you do not specifcy what type of ORDER BY then it will default to ASC.

For example, this query will provide the exact same results as the one above:

SELECT employees.emp_no,
       employees.first_name,
       employees.last_name
FROM employees
ORDER BY employees.emp_no

Most of ORDER BY is used for humans, making it easier to find whether your data were returned correctly. However, there are instances where ORDER BY will actually change the results of your queries, but it will be awhile before we get into those sorts of queries.

Later, we’re going to start working on making our queries efficient and fast, but now I’ll state: Make sure you need your results ordered before you ORDER BY.

It can be hard work for SQL program to order your results, which translates to longer execution times. Something you will want to avoid if you are trying to write a query for speed (which you will when writing code for production software).

Multiple Column Sort

SQL can also do multiple-field sorts. This works by sorting by the first field in the ORDER BY and where there are ties, then sort by the second field.

For example:

SELECT employees.emp_no,
       employees.first_name,
       employees.last_name
FROM employees
ORDER BY employees.last_name ASC, employees.emp_no DESC 

mysql-workbench-export-to-csv

“Aamodt” is the first employee in the last_name field when the ORDER BY is set to ASC, however, there are many “Aamodt”s in this table. This is where the second ORDER BY comes in. The second ORDER BY is set on the emp_no field and is DESC, this is why all the numbers start at the highest values and move towards the lowest. Of course, when the the last_name value changes the emp_no order will restart, still moving from highest to lowest.

mysql-workbench-export-to-csv

Alright, let’s move on. Just remember, ORDER BY is extremely useful for humans, but it makes it slower for computers to process. Therefore, when you write a query, consider your audience.

WHERE

The WHERE clause of a SQL query is a filter. Simple as that. It further limits your results. And it is probably the second most important portion of a query, next to the FROM clause. Reducing your results not only help you find what you need, it also makes it easier on the computer to find the results.

Though, before we get into more detail let’s take a look at an example:

SELECT employees.emp_no         AS Id,
       employees.first_name     AS "First Name",
       employees.last_name      AS "Last Name"
FROM employees
WHERE employees.emp_no = 10006
ORDER BY employees.emp_no, employees.first_name

This returns a single record, which makes sense. We told the SQL program we want emp_no, first_name, last_name from the employees table where the emp_no is equal to 10006.

mysql-workbench-export-to-csv

But, let’s also look at the Database Message

Time Action Message Duration / Fetch
07:35:17 SELECT employees.emp_no, employees.first_name, employees.last_name FROM employees ORDER BY employees.last_name ASC, employees.emp_no DESC LIMIT 0, 1000 1000 row(s) returned 0.152 sec / 0.0035 sec
07:48:56 SELECT employees.emp_no AS Id, employees.first_name AS “First Name”, employees.last_name AS “Last Name” FROM employees WHERE employees.emp_no = 10006 ORDER BY employees.emp_no, employees.first_name LIMIT 0, 1000 1 row(s) returned 0.0036 sec / 0.0000072 sec

Notice how our query for one result took much less time than the query for a 1,000 results? I’ll cover this more later, but felt it was import to point out now. Using the WHERE clause to limit the data to only what you need will greatly increase the efficiency of your query.

Ever been to a cheap buffet with the sign posted on the sneeze-guard reading: “Take only what you will eat!!!” Well, imagine your SQL database has the same sign–you choose what you need with the WHERE clause.

Ok, enough on efficiency for now, let’s focus on how the WHERE clause will allow you to get the results you are after.

In queries we’ve written earlier, we’ve received every row on the database, from every table included in the FROM clause. Now, we are narrowing the results down to those of interest.

This can also be done with strings (text inside of " marks).

SELECT employees.emp_no         AS Id,
       employees.first_name     AS "First Name",
       employees.last_name      AS "Last Name"
FROM employees
WHERE employees.first_name = "Ramzi"
ORDER BY employees.emp_no, employees.first_name

mysql-workbench-export-to-csv

But what if we want to include multiple different employees, but not all? That’s where IN comes…in.

IN

The WHERE clause can be followed by the IN keyword, which is immediately followed by a set of parentheses; inside the parentheses you may put list of values you want to filter on. Each value must be separated by a comma.

For example:

SELECT employees.emp_no         AS Id,
       employees.first_name     AS "First Name",
       employees.last_name      AS "Last Name"
FROM employees
WHERE employees.last_name IN ("Bamford", "Casley", "Benveniste")
ORDER BY employees.last_name ASC, employees.first_name ASC;

mysql-workbench-export-to-csv

This can also be done with numbers

SELECT employees.emp_no         AS Id,
       employees.first_name     AS "First Name",
       employees.last_name      AS "Last Name"
FROM employees
WHERE employees.emp_no IN (422990, 428377)
ORDER BY employees.last_name ASC, employees.first_name ASC;

Greater and Less Than

If the field you are using is numeric data, then you can also use the >, <, <=, and >= comparisons.

SELECT employees.emp_no         AS Id,
       employees.first_name     AS "First Name",
       employees.last_name      AS "Last Name"
FROM employees
WHERE employees.emp_no > 40000
ORDER BY employees.emp_no, employees.first_name;

mysql-workbench-export-to-csv

If you aren’t familiar with the equalities, here’s a breakdown.

  • ”> 5000” will find all values which come after 5000, but does not include 5000 itself
  • ”< 5000” will find all values which come before 5000, but does not include 5000 itself
  • ”>= 5000” will find all values which come after 5000 including 5000 itself
  • ”<= 5000” will find all values which come before 5000 including 5000 itself

Closing Whew, these are the basic of a SQL query, but, it’s just the beginning. There are many more parts to SQL queries, such as AND, OR, <>, !=, JOIN, functions, UNION, DISTINCT–we’ve got a lot more to do. But! No worries, you’ve totally got this.

Don’t believe me? Don’t worry, I’m going to let you prove it to yourself. Let’s do some homework! :)

Homework #1

The following homework will have you take the query provided and modify it to return the described result. Once all queries are completed, fill free to email the queries to me and I’ll “grade” them for you.

For questions #1-6 use the following query:

SELECT *
FROM employees
LEFT JOIN dept_emp
	ON employees.emp_no = dept_emp.emp_no
LEFT JOIN departments
	ON dept_emp.dept_no = departments.dept_no
LEFT JOIN titles
	ON employees.emp_no = titles.emp_no
LEFT JOIN salaries
	ON employees.emp_no = salaries.emp_no;
  • Question #1 – Modify the above query to use table aliases instead of full table names.
  • Question #2 – Modify resulting query to only return results for emp_no, first_name, last_name, dept_name, salary.
  • Question #3 –Modify resulting query to *rename the fields to the following “Employee #”, “First Name”, “Last Name”, “Department #”, and “Salary”.
  • Question #4 –Modify resulting query to list employees by their salaries; order them lowest salary to the highest.
  • Question #5 –While keeping the lowest-to-highest salary order, modify resulting query to list the employees in alphabetical order by their last name where their salaries are tied.
  • Question #6 – Modify resulting query to only provide clients who have make over 50,000

For questions #7-10 use the following query:

SELECT *
FROM employees 			AS e
LEFT JOIN dept_emp		AS de
	ON e.emp_no = de.emp_no
LEFT JOIN departments	AS d
	ON de.dept_no = d.dept_no
LEFT JOIN titles 		AS t
	ON e.emp_no = t.emp_no
LEFT JOIN salaries 		AS s
	ON e.emp_no = s.emp_no
  • Question #7 – Modify the above query to only return results for those with the first name “Yishay”, “Huan”, or “Otmar”
  • Question #8 – Modify resulting query to to also show only their first_name, last_name, and salary.
  • Question #9 – Modify resulting query to to also show what departments they work in.
  • Question #10 – Modify resulting query to also show their hire date.
Beginning MySQL for Data Analysts

I’m usually writing about hacking, robotics, or machine learning, but I thought I’d start journaling thoughts on data analytics, which is how I pay the bills these days. I wanted to begin with a series on MySQL, as I’ve some friends I feel it’d help enter the field. But, I’ll eventually expand the series to include visualizations, analysis, and maybe machine learning. And I hope these articles help someone move from manually generating reports in Excel to writing scripts that’ll automate the boring stuff. As I like to say, “knowing to code gives you data superpowers!”

I’m a professional data analyst, but, if I’m confident of anything, it’s I’ve holes in my understanding. That stated, these articles may contain mistakes. If you spot one, let me know in the comments and I’ll get it fixed quick.

Also, I’m pretty opinionated. I’m sure these opinions will find their way into my writings. When I notice them, I’ll provide a caveat and reasoning for why I hold the opinion.

One last thing, these articles will focus on immediately usable techniques. Honestly, I believe I’ve failed you if you finish an article without a new skill–or, at least an affirmation of existing skill. Don’t get me wrong, I plan to do deep-dives into needed skills, but I believe those are only useful if you have a mental framework to hang them on.

Ok! Let’s do this!

SQL

When getting started in data analytics Structured Query Language (SQL) is a great place to begin. It is a well established data language, having been around since the 70s. The intent of SQL is to empower an individual to retrieve data from a database in an efficient and predictable manner. However, nowadays SQL is used for lots more, such as abstraction, analysis, and semantic changes.

What does it look like? Here’s a example of a SQL query:

SELECT *
FROM employees AS e
LEFT JOIN salaries AS s
	ON e.emp_no = s.emp_no
WHERE e.emp_no = 10004;

The above code is referred to as a query. It’s a question we’d like to get an answer to, written in a language a machine understands. In such, running this query should return all the data needed to answer the question. That’s really what SQL’s about. Writing out a question and getting an answer from the database.

Though! We’re not going to go into those details yet. Right now, let’s setup a practice environment where we can learn to apply concepts along with the concepts themselves.

Sooo Many SQLs

I’d love to tell you SQL is simple. It’s not, well, at least not simple to master. It’s complex–every day I learn something new (one reason I enjoy it). One of its complexities is there are different versions of SQL dialects. Here, we refer to “dialect” as slightly different ways of coding the same thing.

Some of the most common are:

Source / Vendor Common name (Dialectic)
ANSI/ISO Standard SQL/PSM
MariaDB SQL/PSM, PL/SQL
Microsoft / Sybase T-SQL
MySQL SQL/PSM
Oracle PL/SQL
PostgreSQL PL/pgSQL

Let’s make it a bit more confusing. SQL refers to the language, but we often refer to a SQL dialect by it’s vendor or source. Thus, even though MySQL and MariaDB largely speak the same dialect, “SQL / PSM,” we refer to them not by their common name, but by the source name. Thus, “I write MySQL queries.” Or, “At work I use PostgresSQL.”

So which one do you focus on?

Well, we have to start somewhere. I’ve picked MySQL because I use it’s identical twin, MariaDB, at work. It’s a great SQL dialect to begin with, as it’s used by many potential employers.

Source Companies Use
MySQL 58.7%
SQL Server 41.2%
PostgreSQL 32.9%
MongoDB 25.9%
SQLite 19.7%
Redis 18.0%
Elasticsearch 14.1%

Source: Stackoverflow 2018 Developer Survey.

At this point you might be saying, “That’s great? I’ve no idea what any of this means.” No worries! Bookmark this page and come back later. For now, let’s move into setting up a practice MySQL environment.

  • One last note, if you’re going into a job interview it’s a good trick to wait until you hear how they pronounce “SQL” and then say it how they do. As the “correct” pronunciation is “Ess-cue-ell,” however, most professionals I know pronounce it “sequel” (as do I).

sql-pronunciation

Setting up MySQL

These instructions assume you are using Windows. If not, don’t worry, most of them still apply, but you get to skip some steps!

Ok, were are going to install MySQL Workbench. This program will allow us to write SQL queries, send them to a database, get back and view the results.

Preparing to Install MySQL Workbench (Windows Only)

If you are using Windows you need to install software MySQL Workbench uses on Windows.

connecting-to-mysql-server

Click on the link above. Select the vc_redist_x64.exe file and click “Download.” Once the file has finished downloading, install it.

install-vcpp-restributable

MySQL Workbench

Ok! Now we are ready to download and install MySQL. Visit the link below, select your operating system, and choose “Download.”

Select your operating system and hit “Download” download-mysql-workbench

Once the file has finished downloading, run it and follow the install prompts. All choices are fine left on default.

Connecting to the Server

Once you’ve installed MySQL Workbench, open it. When it comes up you should see the main screen, which looks something like: mysql-workbench-welcome-screen

Before we can start querying a database we need to create a database connection. A “connection” here is all the information MySQL Workbench needs to find the database and what permissions you have regarding data access.

connect-mysql-workbench

We will be connecting to a database I’ve setup on a remote computer. Connecting to a remote computers is the most common way to interact with a SQL database, however, later I’ll show you how to build your own database using CSVs. This will be hosted on your local PC.

Ok, back to setting up the remote connection. Click on the circle and plus icon next to “MySQL Connections.” This will cause a screen to pop up for connection information.

Enter the following:

Connection name: maddatum.com
Hostname: maddatum.com
Username: the username I've provided you

Please don’t be shy, if you need a username email me at cthomasbrittain at yahoo dot com. I’ll gladly make you one.

Once you’ve entered the connection information hit “Ok”. You should be brought back to the “Welcome” screen, but now, there will be a connection listed called “maddatum.com”. our-sql-connection

Double click on it. You will most likely get the following warning. sql-connection-warning Click “Continue Anyway” (and if there’s an option, check “Dont Show this Message Again”).

If the connection was successful you should see a screen like: our-sql-connection

Show / Use Databases

Alright! Let’s get into the action. Before we start executing queries let me point out a few things in the user interface: mysql-workbench-interface

Write Query

This area is where you will write queries. Each query should end with a ;, or the MySQL Workbench will get confused and try to jumble two queries together.

View Results

This is the area where the result of whatever command you send the SQL will server will be shown. Often, it will be a table containing data you requested in your query

Database Messages

Here is where you can spot if you written a query incorrectly, as the database will send a message letting you know. Also, the database will tell you when it has successfully returned results from a query, how many results, and how long they took to retrieve. Useful for when you are trying to make a query fast.

Getting Around in MySQL

Let’s send a query to the database. In the query area type:

SHOW databases;

Now, select those text with your mouse and hit the lighting (execute) icon above it. show-databases-command

This will return a list of all the databases found on this server. You should see this in the View Results area. Each SQL server can have multiple databases on it, and they often do. For right now we want to focus on the employees database. show-databases-command

To select a database type USE and then the name of the database. In our case it will be:

USE employees;

Now, highlight the text and hit the execute button.

show-databases-command

This will show the following in the database messages:

13:21:55	USE employees	0 row(s) affected	0.0031 sec
13:21:55	Error loading schema content	Error Code: 1146 Table 'performance_schema.user_variables_by_thread' doesn't exist	

Don’t worry about the error, that’s a product of my hasty setup. The important message is the USE employees message. This means you are now connected to the employees database. Any query you write in this session will now be sent to this specific database.

But, now what? We’ve no idea of what’s on the database. No worries, we’ve a command to see the tables found on this database. If you are not familiar with the term “table,” don’t worry. Just think of a table as a single spreadsheet. It’s a bit more complicated and we will investigate their structure further in a bit. But, right now, the spreadsheet analogy works.

To see all the tables this database contains execute the command:

SHOW tables;

This should return the following table names show-databases-command

By now, you know the next question, “But how do I know what’s in a table?”

You can use the DESCRIBE command to get more information about a table. Let’s take a look at the departments tables.

Type and execute:

DESCRIBE departments;

This should return:

show-databases-command The Field column here gives you the names of all the fields in the departments table. What’s a field? As with table, we will go into them with more depth later. But for now, think of a field as a named column in a spreadsheet.

Our First Query!

Now we know the database, table, and field names, let’s write our first query!

Still in the query area type and execute:

SELECT departments.dept_no, departments.dept_name
FROM departments

This will return all the entries for the fields (columns) dept_no and dept_name for the table (spreadsheet) called departments. You did it! You’re a SQL’er. show-databases-command

What Comes Next?

Lot’s to come! We will learn a bit more about SQL, it’s parts, their proper names. We’ll also dive into the “proper” SQL names for different data parts. And we’ll write tons more queries.

Please feel free to ask any questions in the comments. I’ll answer them ASAP.