Script Python qui stocke 15 ans de données de jeu MLB dans MySQL en 10 minutes (Baseball Hack!)

Ce que je veux dire

** Le script Python, qui a pris un peu plus de 20 minutes pour acquérir et créer des données de correspondance pendant un an, a été amélioré au point où les données de 15 ans peuvent être créées en 10 minutes. ** **

Veuillez consulter ce référentiel pour les résultats.

Shinichi-Nakagawa/py-retrosheet

Nous avons réussi à améliorer les performances d'environ 20 fois par rapport à l'original!

Cependant, je n'ai pas tellement utilisé la technologie, alors j'attends votre avis.

Cette histoire est une suite de l'histoire de baseball PyCon JP 2015.

Début du match ~ Après l'annonce de PyCon JP

C'est le premier message de Qiita. J'ai hâte de travailler avec vous.

À PyCon JP 2015, Baseball Hack! ~ Analyse et visualisation de données à l'aide de Python J'ai téléchargé et migré des données MLB et je les ai analysées et visualisées à l'aide d'un notebook IPython et de pandas! J'ai présenté l'histoire.

Parmi eux, "[py-retrosheet](https://github.com/wellsoliver/py-retrosheet" py-retrosheet ")"

** Téléchargez les données de match et de performance des joueurs de la MLB "[RETROSHEET](http://retrosheet.org/" retrosheet.org ")" et migrez vers votre base de données préférée! ** **

J'ai présenté la bibliothèque appelée "Doya", mais j'ai trouvé diverses choses désagréables dans cette bibliothèque, bien que ce ne soit pas un bogue.

pincer! ~ obscurité py-retrosheet

Sans py-retrosheet, je ne pourrais pas me rendre à PyCon, et je ne pourrais pas jouer avec Joey Bot ou Adam Dan. Je suis très reconnaissant aux auteurs et contributeurs de la bibliothèque, mais lorsque j'ai essayé de l'utiliser, j'ai eu les problèmes non négligeables suivants.

  1. Quoi qu'il en soit, le processus est lent!
  2. La maintenabilité du code lui-même est très mauvaise!
  3. Série Python 2, communément appelée "Legacy Python" uniquement, Python 3 n'est pas pris en charge

Les données de Retrosheet comprennent environ 190 000 à 200 000 lignes d'enregistrements par saison, mais où faites-vous un détour?

** Il a fallu 20 minutes entre le téléchargement des données et la création de la base de données. ** **

Il faut 1 minute pour créer 10 000 lignes de données, ce qui est impossible.

Si vous lisez le code pendant un moment, vous pouvez faire un grattage supplémentaire, émettre une mystérieuse instruction SQL (comme lancer une instruction SELECT avec une vérification de clé unique pour créer une base de données à partir d'un nouvel état) pour tous les enregistrements, etc. J'avais l'habitude d'insérer des lignes une par une, et ainsi de suite.

** Et presque copier et coller! ** **

De plus, comme je veux personnellement faire de Python 3 le développement principal, j'ai pensé qu'une bibliothèque qui ne supporte que Python 2 communément appelée "Legacy Python" ne serait pas cool, j'ai donc décidé d'améliorer la bibliothèque.

Amélioration! ~ Compatible Python 3 et refait

Donc, j'ai immédiatement forké le dépôt d'origine et contribué moi-même. -Nakagawa / py-retrosheet "py-retrosheet") Prêt et a commencé à s'améliorer. Des améliorations ont été apportées dans l'ordre suivant.

  1. Prend en charge Python 3 tout en conservant l'apparence du code d'origine
  2. ~~ Refactoriser l'apparence du code original ~~ (Abandonner) Créer une nouvelle feuille de rétrospective pour Python 3.5

Le problème de la lenteur du traitement est plus grave qu'autre chose, et l'ordre initial est de commencer à partir de là, mais j'ai décidé de viser à comprendre le contenu du code avec précision et à trouver le goulot d'étranglement, et d'abord le code original est Python 3 Je suis parti de l'endroit correspondant. En plus de saisir les spécifications, les conditions de test étaient claires (c'est OK si le même mouvement que l'original est effectué).

Après cela, j'avais l'intention de refactoriser le code d'origine, d'améliorer la maintenabilité et d'attaquer le goulot d'étranglement du retard de vitesse.

** Le code était si moche que j'ai décidé de le refaire. ** **

Prend en charge Python 3

Convertissez le code en utilisant 2to3, qui est un standard lors de la migration de Python 2 vers 3, et répondez tout en éliminant régulièrement les exceptions. Fait. Au fait, j'ai corrigé autant que possible les endroits où cela ne suivait pas pep8 et il semble préférable (ou peut être corrigé) de le réparer.

Le plus dur était qu'une telle déclaration d'importation était inévitablement éclatée.

scripts/download.py


import os
import requests
try:
    # Python 3.x
    from configparser import ConfigParser
    import queue
except ImportError:
    # Python 2.x (Ngo je ne veux pas écrire...orz)
    from ConfigParser import ConfigParser
    import Queue as queue

J'ai réussi à m'échapper en changeant l'unité d'importation et en coupant l'alias.

Après cette rénovation, j'ai émis [Pull Request to Original Author](https://github.com/wellsoliver/py-retrosheet/pull/14 «Upgrade a Python 3»).

Créer une nouvelle py-retrosheet pour Python 3.5

Après avoir supporté Python 3, j'ai pensé à écrire un test et à le refactoriser, mais j'étais en colère et j'ai décidé de le réécrire car il était difficile d'écrire un test et le code n'était pas bon au départ.

py-retrosheet

  1. Téléchargez les données depuis RETROSHEET
  2. Utilisez une bibliothèque appelée Chadwick pour recréer les données dans un format migrable.
  3. Mise en œuvre de Maigure

C'est vraiment simple à faire, mais j'ai senti que je faisais cette chose simple intentionnellement, donc en même temps que de revoir la configuration elle-même, j'ai ajouté les spécifications ajoutées à partir de Python 3.5 (car je veux l'essayer personnellement). Faisons-en une bibliothèque utilisée! J'ai donc apporté quelques améliorations.

De plus, le script original a été conçu pour acquérir et créer des données uniquement pour une saison (un an) par exécution, mais comme il y avait une demande de données pour deux ans ou plus de problèmes réels, nous avons décidé de prendre en charge cela également. ..

Tout d'abord, le téléchargement ressemble à ceci. C'est en fait presque le même que le code d'origine.

scripts/retrosheet_download.py


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Download .
Python 3.5.0+ (don't know about 3.4- and 2.x, sorry)
MySQL 5.6.0+ (don't know about 5.5- , sorry)
"""

import os
import click
from configparser import ConfigParser
from classes.fetcher import Fetcher
from queue import Queue

__author__ = 'Shinichi Nakagawa'


class RetrosheetDownload(object):

    FILES = (
        {
            'name': 'eventfiles',
            'config_flg': 'dl_eventfiles',
            'url': 'eventfiles_url',
            'pattern': r'({year})eve\.zip',
            'download_url': 'http://www.retrosheet.org/events/{year}eve.zip',
        },
        {
            'name': 'gamelogs',
            'config_flg': 'dl_gamelogs',
            'url': 'gamelogs_url',
            'pattern': r'gl({year})\.zip',
            'download_url': 'http://www.retrosheet.org/gamelogs/gl{year}.zip',
        }
    )

    def __init__(self, configfile: str):
        """
        initialize
        :param configfile: configuration file
        """
        # configuration
        self.config = ConfigParser()
        self.config.read(configfile)
        self.num_threads = self.config.getint('download', 'num_threads')
        self.path = self.config.get('download', 'directory')
        self.absolute_path = os.path.abspath(self.path)

    def download(self, queue):
        """
        Download & a Archives
        :param from_year: Season(from)
        :param to_year: Season(to)
        :param configfile: Config file
        """
        threads = []
        for i in range(self.num_threads):
            t = Fetcher(queue, self.absolute_path, {'verbose': self.config.get('debug', 'verbose')})
            t.start()
            threads.append(t)
        for thread in threads:
            thread.join()

    @classmethod
    def run(cls, from_year: int, to_year: int, configfile: str):
        """
        :param from_year: Season(from)
        :param to_year: Season(to)
        :param configfile: Config file
        """
        client = RetrosheetDownload(configfile)

        if not os.path.exists(client.absolute_path):
            print("Directory %s does not exist, creating..." % client.absolute_path)
            os.makedirs(client.absolute_path)

        urls = Queue()
        for year in range(from_year, to_year + 1):
            for _file in RetrosheetDownload.FILES:
                urls.put(_file['download_url'].format(year=year))

        client.download(urls)


@click.command()
@click.option('--from_year', '-f', default=2001, help='From Season')
@click.option('--to_year', '-t', default=2014, help='To Season')
@click.option('--configfile', '-c', default='config.ini', help='Config File')
def download(from_year, to_year, configfile):
    """
    :param from_year: Season(from)
    :param to_year: Season(to)
    :param configfile: Config file
    """
    # from <= to check
    if from_year > to_year:
        print('not From <= To({from_year} <= {to_year})'.format(from_year=from_year, to_year=to_year))
        raise SystemExit

    RetrosheetDownload.run(from_year, to_year, configfile)


if __name__ == '__main__':
    download()

Arrêtez de recevoir des pages inutiles et de gratter

De plus, concernant le problème de la "gestion complexe des arguments autour de la ligne de commande et de la configuration", qui était un problème de maintenance pour py-retrosheet en général, le premier est cliquez, et ce dernier a été résolu en agrégeant l'emplacement d'acquisition de la configuration dans le traitement initial de la classe.

Cela s'applique également à l'acquisition et à la création de données, qui seront introduites ultérieurement.

Vient ensuite la partie pour obtenir des données en utilisant chadwick.

scripts/parse_csv.py


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Parse to Event Files, Game Logs and Roster.
Python 3.5.0+ (don't know about 3.4- and 2.x, sorry)
"""
import os
import subprocess
import click
from configparser import ConfigParser

__author__ = 'Shinichi Nakagawa'


class ParseCsv(object):

    CW_EVENT = '{chadwick_path}cwevent'
    CW_GAME = '{chadwick_path}cwgame'
    CW_EVENT_CMD = '{chadwick_path}cwevent -q -n -f 0-96 -x 0-62 -y {year} {year}*.EV* > {csvpath}/events-{year}.csv'
    CW_GAME_CMD = '{chadwick_path}cwgame -q -n -f 0-83 -y {year} {year}*.EV* > {csvpath}/games-{year}.csv'
    EV_FILE_PATTERN = '{path}/{year}*.EV*'
    EVENT_FILE = '{csvpath}/events-{year}.csv'
    GAME_FILE = '{csvpath}/games-{year}.csv'
    CSV_PATH = 'csv'

    def __init__(self):
        pass

    @classmethod
    def exists_chadwick(cls, chadwick_path: str):
        """
        exists chadwick binary
        :param chadwick_path: chadwick path
        :return: True or False
        """
        if os.path.exists(chadwick_path) \
            & os.path.exists(cls.CW_EVENT.format(chadwick_path=chadwick_path)) \
                & os.path.exists(cls.CW_GAME.format(chadwick_path=chadwick_path)):
            return True
        return False

    @classmethod
    def generate_files(
            cls,
            year: int,
            cmd_format: str,
            filename_format: str,
            chadwick_path: str,
            verbose: bool,
            csvpath: str
    ):
        """
        Generate CSV file
        :param year: Season
        :param cmd_format: Command format
        :param filename_format: Filename format
        :param chadwick_path: Chadwick Command Path
        :param verbose: Debug flg
        :param csvpath: csv output path
        """
        cmd = cmd_format.format(csvpath=csvpath, year=year, chadwick_path=chadwick_path)
        filename = filename_format.format(csvpath=csvpath, year=year)
        if os.path.isfile(filename):
            os.remove(filename)
        if verbose:
            print('calling {cmd}'.format(cmd=cmd))
        subprocess.call(cmd, shell=True)

    @classmethod
    def generate_retrosheet_files(
            cls,
            from_year: int,
            to_year: int,
            chadwick_path: str,
            verbose: str,
            csvpath: str
    ):
        """
        Generate CSV file
        :param from_year: Season(from)
        :param to_year: Season(to)
        :param chadwick_path: Chadwick Command Path
        :param verbose: Debug flg
        :param csvpath: csv output path
        """
        # generate files
        for year in [year for year in range(from_year, to_year + 1)]:
            # game
            ParseCsv.generate_files(
                year=year,
                cmd_format=ParseCsv.CW_GAME_CMD,
                filename_format=ParseCsv.GAME_FILE,
                chadwick_path=chadwick_path,
                verbose=verbose,
                csvpath=csvpath
            )
            # event
            ParseCsv.generate_files(
                year=year,
                cmd_format=ParseCsv.CW_EVENT_CMD,
                filename_format=ParseCsv.EVENT_FILE,
                chadwick_path=chadwick_path,
                verbose=verbose,
                csvpath=csvpath
            )

    @classmethod
    def run(cls, from_year: int, to_year: int, configfile: str):
        """
        :param from_year: Season(from)
        :param to_year: Season(to)
        :param configfile: Config file
        """
        config = ConfigParser()
        config.read(configfile)
        verbose = config.getboolean('debug', 'verbose')
        chadwick = config.get('chadwick', 'directory')
        path = os.path.abspath(config.get('download', 'directory'))
        csv_path = '{path}/csv'.format(path=path)

        # command exists check
        if not cls.exists_chadwick(chadwick):
            print('chadwick does not exist in {chadwick} - exiting'.format(chadwick=chadwick))
            raise SystemExit

        # make directory
        os.chdir(path)
        if not os.path.exists(ParseCsv.CSV_PATH):
            os.makedirs(ParseCsv.CSV_PATH)

        # generate files
        cls.generate_retrosheet_files(
            from_year=from_year,
            to_year=to_year,
            chadwick_path=chadwick,
            verbose=verbose,
            csvpath=csv_path
        )

        # change directory
        os.chdir(os.path.dirname(os.path.abspath(__file__)))


@click.command()
@click.option('--from_year', '-f', default=2001, help='From Season')
@click.option('--to_year', '-t', default=2014, help='To Season')
@click.option('--configfile', '-c', default='config.ini', help='Config File')
def create_retrosheet_csv(from_year, to_year, configfile):
    """
    :param from_year: Season(from)
    :param to_year: Season(to)
    :param configfile: Config file
    """
    # from <= to check
    if from_year > to_year:
        print('not From <= To({from_year} <= {to_year})'.format(from_year=from_year, to_year=to_year))
        raise SystemExit
    ParseCsv.run(from_year, to_year, configfile)


if __name__ == '__main__':
    create_retrosheet_csv()


Je traitais les données du jeu (journal de jeu) et les données d'événements (journal des événements) pendant le jeu, comme au bâton et en volant séparément, mais je faisais la même chose (seule la commande utilisée et la destination de sortie sont différentes), donc Le traitement est regroupé en méthodes et la définition est prise de l'extérieur. C'est la même chose pour l'implémentation de la création de données.

scripts/retrosheet_mysql.py


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Migrate Database, Game Logs and Roster.
Python 3.5.0+ (don't know about 3.4- and 2.x, sorry)
MySQL 5.6.0+ (don't know about 5.5- , sorry)
"""
import os
import csv
import click
import sqlalchemy
from glob import glob
from configparser import ConfigParser, NoOptionError

__author__ = 'Shinichi Nakagawa'


class RetrosheetMySql(object):

    DATABASE_ENGINE = 'mysql+pymysql'
    ENGINE = '{engine}://{user}:{password}@{host}/{database}'
    TABLES = (
        {
            'name': 'teams',
            'header': False,
            'year': False,
            'mask': '{path}/TEAM{year}*',
            'select': "SELECT * FROM teams WHERE team_id = '{key_0}'",
            'where_index': [0],
            'insert': 'INSERT INTO teams VALUES {values}',
        },
        {
            'name': 'rosters',
            'header': False,
            'year': True,
            'mask': '{path}/*{year}*.ROS',
            'select': "SELECT * FROM rosters WHERE year = {year} AND player_id = '{key_0}' AND team_tx = '{key_1}'",
            'where_index': [0, 5],
            'insert': 'INSERT INTO rosters VALUES {values}',
        },
        {
            'name': 'games',
            'header': True,
            'year': False,
            'mask': '{path}/csv/games-{year}*.csv',
            'select': "SELECT * FROM games WHERE game_id = '{key_0}'",
            'where_index': [0],
            'insert': 'INSERT INTO games({columns}) VALUES {values}',
        },
        {
            'name': 'events',
            'header': True,
            'year': False,
            'mask': '{path}/csv/events-{year}*.csv',
            'select': "SELECT * FROM events WHERE game_id = '{key_0}' AND event_id = '{key_1}'",
            'where_index': [0, 96],
            'insert': 'INSERT INTO events({columns}) VALUES {values}',
        },
    )

    def __init__(self, configfile: str):
        """
        initialize
        :param configfile: configuration file
        """
        # configuration
        config = ConfigParser()
        config.read(configfile)
        self.path = os.path.abspath(config.get('download', 'directory'))
        self.record_check = config.getboolean('retrosheet_mysql', 'record_check')
        self.multiple_insert_rows = config.getint('retrosheet_mysql', 'multiple_insert_rows')
        # connection
        self.connection = self._generate_connection(config)

    @classmethod
    def _generate_connection(cls, config: ConfigParser):
        """
        generate database connection
        :param config: ConfigParser object
        :return:
        """

        try:
            database_engine = cls.DATABASE_ENGINE
            database = config.get('database', 'database')

            host = config.get('database', 'host')
            user = config.get('database', 'user')
            password = config.get('database', 'password')
        except NoOptionError:
            print('Need to define engine, user, password, host, and database parameters')
            raise SystemExit

        db = sqlalchemy.create_engine(
            cls.ENGINE.format(
                engine=database_engine,
                user=user,
                password=password,
                host=host,
                database=database,
            )
        )

        return db.connect()

    @classmethod
    def _exists_record(cls, year: int, table: dict, csv_row: list, connection):
        where = {'key_{i}'.format(i=i): csv_row[v] for i, v in enumerate(table['where_index'])}
        where['year'] = year
        sql = table['select'].format(**where)
        res = connection.execute(sql)
        if res.rowcount > 0:
            return True
        return False

    def _multiple_insert(self, query: str, columns: list, values: list):
        params = {
            'columns': columns,
            'values': ', '.join(values),
        }
        sql = query.format(**params)
        try:
            self.connection.execute(sql)
        except Exception as e:
            print(e)
            raise SystemExit

    def _create_record(self, year: int, csv_file: str, table: dict):
        reader = csv.reader(open(csv_file))
        headers = []
        values = []
        if table['header']:
            headers = next(reader)
        columns = ', '.join(headers)
        for row in reader:
            # Record Exists Check
            if self.record_check and RetrosheetMySql._exists_record(year, table, row, self.connection):
                continue
            # append record values
            if table['year']: row.insert(0, str(year))
            values.append('("{rows}")'.format(rows='", "'.join(row)))
            if len(values) == self.multiple_insert_rows:
                # inserts
                self._multiple_insert(table['insert'], columns, values)
                values = []
        if len(values) > 0:
            # inserts
            self._multiple_insert(table['insert'], columns, values)

    def execute(self, year: int):
        for table in self.TABLES:
            for csv_file in glob(table['mask'].format(path=self.path, year=year)):
                self._create_record(year, csv_file, table)

    @classmethod
    def run(cls, from_year: int, to_year: int, configfile: str):
        """
        :param from_year: Season(from)
        :param to_year: Season(to)
        :param configfile: Config file
        """
        client = RetrosheetMySql(configfile)
        for year in range(from_year, to_year + 1):
            client.execute(year)
        client.connection.close()


@click.command()
@click.option('--from_year', '-f', default=2001, help='From Season')
@click.option('--to_year', '-t', default=2014, help='To Season')
@click.option('--configfile', '-c', default='config.ini', help='Config File')
def migration(from_year, to_year, configfile):
    """
    :param from_year: Season(from)
    :param to_year: Season(to)
    :param configfile: Config file
    """
    # from <= to check
    if from_year > to_year:
        print('not From <= To({from_year} <= {to_year})'.format(from_year=from_year, to_year=to_year))
        raise SystemExit

    RetrosheetMySql.run(from_year, to_year, configfile)


if __name__ == '__main__':
    migration()

Les meilleures améliorations de cette implémentation,

La vérification de l'instruction de sélection est implémentée mais non utilisée (elle peut être utilisée en modifiant le paramètre de configuration).

La vitesse s'est considérablement améliorée simplement en changeant l'instruction d'insertion, qui se comportait bien ligne par ligne, en 1000 lignes (qui peuvent également être ajustées avec config).

De plus, les perspectives générales du code se sont améliorées, il est donc facile de gérer l'ajout de données all-star et de séries éliminatoires que vous n'avez pas encore utilisées!

référence

Après m'être amélioré jusqu'à présent, j'ai essayé de comparer les données de 2014 (environ 190 000 lignes).

La cible est

est. La mesure est

Je l'ai fait à la condition.

#Avant amélioration
$ time python download.py -y 2014
Queuing up Event Files for download (2014 only).
Queuing up Game Logs for download (2014 only).
Fetching 2014eve.zip
Fetching gl2014.zip
Zip file detected. Extracting gl2014.zip
Zip file detected. Extracting 2014eve.zip

real	0m5.816s
user	0m0.276s
sys	0m0.066s
$ time python parse.py -y 2014
calling '/usr/local/bin//cwevent -q -n -f 0-96 -x 0-62 -y 2014 2014*.EV* > /Users/shinyorke_mbp/PycharmProjects/py-retrosheet/scripts/files/csv/events-2014.csv'
calling '/usr/local/bin//cwgame -q -n -f 0-83 -y 2014 2014*.EV* > /Users/shinyorke_mbp/PycharmProjects/py-retrosheet/scripts/files/csv/games-2014.csv'
processing TEAM2014
processing ANA2014.ROS
processing ARI2014.ROS
processing ATL2014.ROS
processing BAL2014.ROS
processing BOS2014.ROS
processing CHA2014.ROS
processing CHN2014.ROS
processing CIN2014.ROS
processing CLE2014.ROS
processing COL2014.ROS
processing DET2014.ROS
processing HOU2014.ROS
processing KCA2014.ROS
processing LAN2014.ROS
processing MIA2014.ROS
processing MIL2014.ROS
processing MIN2014.ROS
processing NYA2014.ROS
processing NYN2014.ROS
processing OAK2014.ROS
processing PHI2014.ROS
processing PIT2014.ROS
processing SDN2014.ROS
processing SEA2014.ROS
processing SFN2014.ROS
processing SLN2014.ROS
processing TBA2014.ROS
processing TEX2014.ROS
processing TOR2014.ROS
processing WAS2014.ROS
processing /Users/shinyorke_mbp/PycharmProjects/py-retrosheet/scripts/files/csv/games-2014.csv
processing /Users/shinyorke_mbp/PycharmProjects/py-retrosheet/scripts/files/csv/events-2014.csv

real	19m53.224s
user	13m16.074s
sys	0m26.294s

#Après amélioration
$ time python retrosheet_download.py -f 2014 -t 2014
Fetching 2014eve.zip
Fetching gl2014.zip
Zip file detected. Extracting gl2014.zip
Zip file detected. Extracting 2014eve.zip

real	0m4.630s
user	0m0.217s
sys	0m0.055s
$ time python parse_csv.py -f 2014 -t 2014
calling /usr/local/bin/cwgame -q -n -f 0-83 -y 2014 2014*.EV* > /Users/shinyorke_mbp/PycharmProjects/py-retrosheet/scripts/files/csv/games-2014.csv
calling /usr/local/bin/cwevent -q -n -f 0-96 -x 0-62 -y 2014 2014*.EV* > /Users/shinyorke_mbp/PycharmProjects/py-retrosheet/scripts/files/csv/events-2014.csv

real	0m8.713s
user	0m8.321s
sys	0m0.221s
$ time python retrosheet_mysql.py -f 2014 -t 2014

real	0m21.435s
user	0m3.580s
sys	0m0.416s

** Le processus qui prenait à l'origine 20 minutes a été amélioré à moins d'une minute! ** **

Avec cela, même si vous voulez une grande quantité de données de jeu / siège, vous pouvez facilement l'obtenir ou le créer.

Essayez de créer des données pendant 15 ans

Cela devient intéressant, j'ai donc décidé de faire des données sur 15 ans de 2000 à 2014.

Le nombre total d'enregistrements est d'environ 2,9 millions et non d'environ 3 millions.

J'ai récemment transcrit le script pour tout exécuter en même temps.

scripts/migration.py


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Migrate Retrosheet Database.
Python 3.5.0+ (don't know about 3.4- and 2.x, sorry)
MySQL 5.6.0+ (don't know about 5.5- , sorry)
"""
import logging
import click
from retrosheet_download import RetrosheetDownload
from retrosheet_mysql import RetrosheetMySql
from parse_csv import ParseCsv

__author__ = 'Shinichi Nakagawa'


@click.command()
@click.option('--from_year', '-f', default=2010, help='From Season')
@click.option('--to_year', '-t', default=2014, help='To Season')
@click.option('--configfile', '-c', default='config.ini', help='Config File')
def main(from_year, to_year, configfile):
    """
    :param from_year: Season(from)
    :param to_year: Season(to)
    :param configfile: Config file
    """
    # from <= to check
    if from_year > to_year:
        print('not From <= To({from_year} <= {to_year})'.format(from_year=from_year, to_year=to_year))
        raise SystemExit

    # logging setting
    logging.basicConfig(
        level=logging.INFO,
        format="Time:%(asctime)s.%(msecs)03d\t message:%(message)s",
        datefmt="%Y-%m-%d %H:%M:%S"
    )

    # Download
    logging.info('File Download Start({from_year}-{to_year})'.format(from_year=from_year, to_year=to_year))
    RetrosheetDownload.run(from_year, to_year, configfile)
    logging.info('File Download End')

    # Parse Csv
    logging.info('Csv Parse Start({from_year}-{to_year})'.format(from_year=from_year, to_year=to_year))
    ParseCsv.run(from_year, to_year, configfile)
    logging.info('Csv Parse End')

    # Migrate MySQL Database
    logging.info('Migrate Database Start({from_year}-{to_year})'.format(from_year=from_year, to_year=to_year))
    RetrosheetMySql.run(from_year, to_year, configfile)
    logging.info('Migrate Database End')


if __name__ == '__main__':
    main()

Cliquez ici pour les résultats

$ python migration.py -f 2000 -t 2014
Time:2015-11-15 15:23:48.291	 message:File Download Start(2000-2014)
Directory /Users/shinyorke_mbp/PycharmProjects/hatteberg/py-retrosheet/scripts/files does not exist, creating...
Time:2015-11-15 15:23:59.673	 message:File Download End
Time:2015-11-15 15:23:59.673	 message:Csv Parse Start(2000-2014)
Time:2015-11-15 15:26:37.219	 message:Csv Parse End
Time:2015-11-15 15:26:37.220	 message:Migrate Database Start(2000-2014)
Time:2015-11-15 15:32:50.070	 message:Migrate Database End

** Cela a pris moins de 10 minutes entre l'acquisition des données et l'achèvement de la configuration! !! !! ** **

Résumé

Cela rendra un peu plus amusant Hack de baseball!

Recommended Posts

Script Python qui stocke 15 ans de données de jeu MLB dans MySQL en 10 minutes (Baseball Hack!)
Un ensemble de fichiers de script qui font wordcloud avec Python3
[Baseball Hack] J'ai essayé de copier le script d'acquisition de données de score et de note Python MLB avec Go en une demi-journée
Lecture de code de faker, une bibliothèque qui génère des données de test en Python
Environnement enregistré pour l'analyse des données avec Python
[Python] Un programme qui trouve le nombre d'étapes le plus court dans un jeu qui traverse les nuages
Publication d'une bibliothèque qui masque les données de caractères dans les images Python
[Python] Création d'un outil GUI qui traite automatiquement le CSV des données d'élévation de température dans Excel
Créons un script qui s'enregistre avec Ideone.com en Python.
Un script python qui convertit les données Oracle Database en csv
Script Python qui compare le contenu de deux répertoires
Une analyse simple des données de Bitcoin fournie par CoinMetrics en Python
Une fonction qui mesure le temps de traitement d'une méthode en python
Implémentation du jeu de vie en Python
Comment envoyer une image visualisée des données créées en Python à Typetalk
Résumé du livre électronique Python utile pour l'analyse de données gratuite
J'ai essayé d'implémenter le jeu de cartes de Trump en Python
Afficher une liste d'alphabets en Python 3
Exécuter l'interpréteur Python dans le script
J'ai créé un programme en Python qui lit les données FX CSV et crée un grand nombre d'images de graphiques
Opérations de base sur les blocs de données écrites par des débutants au cours d'une semaine d'apprentissage de Python
Jusqu'à ce que vous obteniez des données quotidiennes pour plusieurs années de stock japonais et que vous les sauvegardiez dans un seul CSV (Python)
Dessiner un graphique d'une fonction quadratique en Python
Un mémo que j'ai écrit un tri rapide en Python
Récupérer l'appelant d'une fonction en Python
J'ai essayé de jouer à un jeu de frappe avec Python
Un programme qui supprime les instructions en double en Python
Copiez la liste en Python
Visualisation en temps réel des données thermographiques AMG8833 en Python
Réécrire des éléments dans une boucle de listes (Python)
Créez un jeu Janken en une seule ligne (python)
Analyse de données en Python: une note sur line_profiler
"Kit Python" qui appelle des scripts Python depuis Swift
L'histoire de la lecture des données HSPICE en Python
Créez un tracé de R semblable à un joyplot avec python
Appel de scripts Python à partir de Python intégré en C ++ / C ++
Sortie sous la forme d'un tableau python
Touchons une partie de l'apprentissage automatique avec Python
[Docker] Créez un environnement jupyterLab (python) en 3 minutes!
Résumé des méthodes d'analyse de données statistiques utilisant Python qui peuvent être utilisées en entreprise
Un script python qui obtient le nombre de travaux pour une condition spécifiée sur Indeed.com
Une note qui implémente une tâche en Python qui charge un fichier GCS dans BigQuery
Consolider un grand nombre de fichiers CSV dans des dossiers avec python (données sans en-tête)
Programme qui résume les données csv de l’historique des transactions de l’action SBI Securities [Python3]
J'ai créé un programme en Python qui change les données de 1 minute de FX en une heure arbitraire (1 heure, etc.)