Compute leaderboard of competitive riders throughout a season
You should strive to reduce the number of queries to your DB. Instead of updating the whole table, row by row, each time you compute a new value, you could:


load the data (using pd.read_sql_table);
perform the computation of the positions (this may require you to use None or pd.np.NaN instead of 'NULL');
write the whole new table at once at the end (using df.to_sql).


The rewrite could look like:

def set_positions():
    """Add positions to the DB for each column in our overall table"""
    engine = sql.create_engine(MYSQL_CON_STRING)
    connection = engine.connect()
    trans = connection.begin()

    # Men, Women, Junior
    for category in CATEGORIES:
        table = category + '_overall'
        df = pd.read_sql_table(table, engine)
        compute_positions_for_table(df)
        df.to_sql(table, engine, if_exists='replace')
        trans.commit()
    connection.close()


def compute_positions_for_table(df):
    points_columns_list = [col for col in df.columns
                           if 'total' in col and 'position' not in col]

    # Calculate the rank based on current total points column and
    # insert into current column index + 1
    for points_column in points_columns_list:
        position_index = df.columns.get_loc(points_column) + 1
        position_column = df.columns[position_index]
        current_points = df.groupby('year')
        position = current_points[points_column].rank(ascending=0, method='min').astype(int)
        df.loc[:, position_column] = position.values

        # No points awarded in this and/or previous rounds. Everyone is 
        # at 0 so everyone is rank 1
        df.loc[(df[points_column] == 0)
               &amp; (df[position_column] == 1), position_column] = None


And even if you can't "replace" the table, you should still update the table only once at the end of the computation rather than at each new column computation.