** 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.
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.
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.
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.
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.
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. ** **
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»).
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
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!
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.
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! !! !! ** **
Cela rendra un peu plus amusant Hack de baseball!
Recommended Posts