From 20856a093a339be1c47da53ac888389fb4e9d6bf Mon Sep 17 00:00:00 2001 From: Kevin Alex Zhang Date: Thu, 4 Jun 2020 01:30:30 -0400 Subject: [PATCH 1/6] Added S3 download --- benchmark/README.md | 1 + benchmark/benchmark.py | 0 datatracer/data.py | 27 +++++++++++++++++++++++++++ setup.py | 3 ++- 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 benchmark/README.md create mode 100644 benchmark/benchmark.py diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..181eaeb --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1 @@ +# Benchmarking DataTracer diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py new file mode 100644 index 0000000..e69de29 diff --git a/datatracer/data.py b/datatracer/data.py index 400acfe..35507e6 100644 --- a/datatracer/data.py +++ b/datatracer/data.py @@ -6,7 +6,12 @@ """ import os import shutil +from io import BytesIO +from urllib.parse import urljoin +from urllib.request import urlopen +from zipfile import ZipFile +import boto3 import pandas as pd from metad import MetaData @@ -91,3 +96,25 @@ def get_demo_data(path='datatracer_demo', force=False): demo_path = os.path.join(os.path.dirname(__file__), 'datasets') shutil.copytree(demo_path, path) + + +def download(): + BUCKET_NAME = 'tracer-data' # Bucket where the datasets are stored + DATA_URL = 'http://{}.s3.amazonaws.com/'.format(BUCKET_NAME) + + homedir = os.path.expanduser("~/") + if homedir == "~/": + raise ValueError("Could not find a default download directory") + data_dir = os.path.join(homedir, "tracer_data") + + client = boto3.client('s3') + for dataset in client.list_objects(Bucket=BUCKET_NAME)['Contents']: + dataset_name = dataset['Key'].replace(".zip", "") + dataset_path = os.path.join(data_dir, dataset_name) + if os.path.exists(dataset_path): + print("Skipping %s" % dataset_name) + else: + print("Downloading %s" % dataset_name) + with urlopen(urljoin(DATA_URL, dataset['Key'])) as fp: + with ZipFile(BytesIO(fp.read())) as zipfile: + zipfile.extractall(dataset_path) diff --git a/setup.py b/setup.py index 9159b71..44f699c 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ history = history_file.read() install_requires = [ + 'boto3>=1.13,<2', 'pandas>=0.23.4,<0.25', 'scikit-learn>=0.20.0,<0.21', 'numpy<1.17,>=1.15.2', @@ -100,7 +101,7 @@ keywords='datatracer data-tracer Data Tracer', name='datatracer', packages=find_packages(include=['datatracer', 'datatracer.*']), - python_requires='>=3.5,<3.8', + python_requires='>=3.5,<=3.8', setup_requires=setup_requires, test_suite='tests', tests_require=tests_require, From 699adaed0148290d9471dc7e0e76c84493fc7e38 Mon Sep 17 00:00:00 2001 From: Kevin Alex Zhang Date: Sat, 6 Jun 2020 15:58:31 -0400 Subject: [PATCH 2/6] Added primary/foreign evaluation --- benchmark/benchmark.py | 95 ++++++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index e69de29..3ad17c2 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -0,0 +1,95 @@ +from datatracer import load_datasets, DataTracer + +def evaluate_primary_key(solver, metadata, tables): + """Evaluate a primary key solver. + + This computes the accuracy of a primary key solver on the given dataset. It + skips tables without primary keys as well as tables which have composite + primary keys. + + Args: + solver (DataTracer): + A DataTracer instance which implements primary key detection. + metadata (MetaData): + A MetaData instance which describes the tables. + tables (dict): + A dictionary mapping table names to dataframes + + Returns: + dict: + A dictionary mapping metric names to values. + """ + y_true = {} + for table in metadata.get_tables(): + if "primary_key" not in table: + continue # Skip tables without primary keys + if not isinstance(table["primary_key"], str): + continue # Skip tables with composite primary keys + y_true[table["name"]] = table["primary_key"] + + correct, total = 0, 0 + y_pred = solver.solve(tables) + for table_name, primary_key in y_true.items(): + if y_pred.get(table_name) == primary_key: + correct += 1 + total += 1 + accuracy = correct / total + + return { + "accuracy": accuracy + } + +def evaluate_foreign_key(solver, metadata, tables): + """Evaluate a foreign key solver. + + This computes the precision, recall, and f1 score of a foreign key solver + on the given dataset. It skips composite foreign primary keys. + + Args: + solver (DataTracer): + A DataTracer instance which implements foreign key detection. + metadata (MetaData): + A MetaData instance which describes the tables. + tables (dict): + A dictionary mapping table names to dataframes + + Returns: + dict: + A dictionary mapping metric names to values. + """ + y_true = set() + for fk in metadata.get_foreign_keys(): + if not isinstance(fk["field"], str): + continue # Skip composite foreign keys + y_true.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) + + y_pred = set() + best_precision, best_recall, best_f1 = float("-inf"), float("-inf"), float("-inf") + for fk in solver.solve(tables): + y_pred.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) + + precision = len(y_true.intersection(y_pred)) / len(y_pred) + recall = len(y_true.intersection(y_pred)) / len(y_true) + f1 = 2.0 * precision * recall / (precision + recall) + + if f1 > best_f1: + best_precision = precision + best_recall = recall + best_f1 = f1 + + return { + "precision": best_precision, + "recall": best_recall, + "f1": best_f1 + } + +if __name__ == "__main__": + datasets = load_datasets("../datatracer/datasets") + + solver = DataTracer.load('datatracer.primary_key.basic') + for dataset_name, (metadata, tables) in datasets.items(): + print(dataset_name, evaluate_primary_key(solver, metadata, tables)) + + solver = DataTracer.load('datatracer.foreign_key.standard') + for dataset_name, (metadata, tables) in datasets.items(): + print(dataset_name, evaluate_foreign_key(solver, metadata, tables)) diff --git a/setup.py b/setup.py index 44f699c..3b7dd33 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ 'falcon>=2.0.0,<3', 'hug>=2.6.1,<3', 'pyyaml>=5.3.1,<6', - 'tqdm>=4.46.1,<5', + 'tqdm>=4,<5', ] setup_requires = [ From 422fad84e5e63bf381e0d7cdc9a8d3a644f833ca Mon Sep 17 00:00:00 2001 From: Kevin Alex Zhang Date: Mon, 8 Jun 2020 15:01:04 -0400 Subject: [PATCH 3/6] Added inference time --- benchmark/README.md | 59 ++++++++++++++++++++++++++++++++++++++++++ benchmark/benchmark.py | 31 ++++++++++++---------- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/benchmark/README.md b/benchmark/README.md index 181eaeb..4150079 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1 +1,60 @@ # Benchmarking DataTracer +This directory contains code for benchmarking the performance of `DataTracer` +on user-supplied datasets. The datasets for benchmarking can be found in the +`s3://tracer-data` bucket. + +## Primary Key +Primary key detection is evaluated by: + + - Accuracy. The percent of tables where the primary key was correctly identified. + - Inference time. The amount of time to infer the primary key for all tables. + +We will use leave-one-out validation and report the test performance on each dataset +in the S3 bucket. + +## Foreign Key +Foreign key detection is evaluated by: + + - F1. + - Recall. + - Precision. + - Inference time. + +Note that this assumes that the foreign key primitive returns a set of foreign keys; +in other words, for models that return a score for each candidate foreign key, this +assumes that thresholding is done. + +We will use leave-one-out validation and report the test performance on each dataset +in the S3 bucket. + +## Foreign Key (No Thesholding) +Foreign key detection (no thresholding) is evaluated by: + + - Best F1. + - Best Recall. + - Best Precision. + - Inference time. + +Note that this assumes that the foreign key primitive returns a ranking of foreign +keys. For each threshold - i.e. take the top N predicted foreign keys - the F1 score +is computed and only the threshold that produces the best F1 score is selected. It +returns the corresponding F1/Recall/Precision values. + +We will use leave-one-out validation and report the test performance on each dataset +in the S3 bucket. + +## Column Map +Column map detection is evaluated by: + + - F1. + - Recall. + - Precision. + - Inference time. + +Note that this assumes that the column map primitive returns a set of columns that it +thinks contributed to the target derived column. Since each dataset can have multiple +derived columns, this will report a F1/recall/precision/time tuple for each derived +column in the dataset. + +We will use leave-one-out validation and report the test performance on each dataset +in the S3 bucket. diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index 3ad17c2..d9d3107 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -1,3 +1,4 @@ +from time import time from datatracer import load_datasets, DataTracer def evaluate_primary_key(solver, metadata, tables): @@ -28,7 +29,9 @@ def evaluate_primary_key(solver, metadata, tables): y_true[table["name"]] = table["primary_key"] correct, total = 0, 0 + start = time() y_pred = solver.solve(tables) + end = time() for table_name, primary_key in y_true.items(): if y_pred.get(table_name) == primary_key: correct += 1 @@ -36,7 +39,8 @@ def evaluate_primary_key(solver, metadata, tables): accuracy = correct / total return { - "accuracy": accuracy + "accuracy": accuracy, + "inference_time": end - start } def evaluate_foreign_key(solver, metadata, tables): @@ -63,24 +67,23 @@ def evaluate_foreign_key(solver, metadata, tables): continue # Skip composite foreign keys y_true.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) + start = time() + fk_pred = solver.solve(tables) + end = time() + y_pred = set() - best_precision, best_recall, best_f1 = float("-inf"), float("-inf"), float("-inf") - for fk in solver.solve(tables): + for fk in fk_pred: y_pred.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) - precision = len(y_true.intersection(y_pred)) / len(y_pred) - recall = len(y_true.intersection(y_pred)) / len(y_true) - f1 = 2.0 * precision * recall / (precision + recall) - - if f1 > best_f1: - best_precision = precision - best_recall = recall - best_f1 = f1 + precision = len(y_true.intersection(y_pred)) / len(y_pred) + recall = len(y_true.intersection(y_pred)) / len(y_true) + f1 = 2.0 * precision * recall / (precision + recall) return { - "precision": best_precision, - "recall": best_recall, - "f1": best_f1 + "precision": precision, + "recall": recall, + "f1": f1, + "inference_time": end - start } if __name__ == "__main__": From 4a7476218cba3ff27c7db7393328d4954c0529ee Mon Sep 17 00:00:00 2001 From: Kevin Alex Zhang Date: Tue, 9 Jun 2020 12:57:10 -0400 Subject: [PATCH 4/6] Added dask-based benchmarks --- benchmark/README.md | 23 +--- benchmark/benchmark.py | 298 +++++++++++++++++++++++++++++++++-------- datatracer/data.py | 27 ---- setup.py | 8 +- 4 files changed, 252 insertions(+), 104 deletions(-) diff --git a/benchmark/README.md b/benchmark/README.md index 4150079..e51398a 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -3,6 +3,13 @@ This directory contains code for benchmarking the performance of `DataTracer` on user-supplied datasets. The datasets for benchmarking can be found in the `s3://tracer-data` bucket. +Each benchmark - `primary`, `foreign`, and `column` - can be executed by +running the following command + +> datatracer-benchmark --output /path/to/results.csv + +which will (optionally) generate a CSV file with the benchmark results. + ## Primary Key Primary key detection is evaluated by: @@ -27,22 +34,6 @@ assumes that thresholding is done. We will use leave-one-out validation and report the test performance on each dataset in the S3 bucket. -## Foreign Key (No Thesholding) -Foreign key detection (no thresholding) is evaluated by: - - - Best F1. - - Best Recall. - - Best Precision. - - Inference time. - -Note that this assumes that the foreign key primitive returns a ranking of foreign -keys. For each threshold - i.e. take the top N predicted foreign keys - the F1 score -is computed and only the threshold that produces the best F1 score is selected. It -returns the corresponding F1/Recall/Precision values. - -We will use leave-one-out validation and report the test performance on each dataset -in the S3 bucket. - ## Column Map Column map detection is evaluated by: diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index d9d3107..7aa8873 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -1,25 +1,58 @@ +import argparse +import os +from io import BytesIO from time import time -from datatracer import load_datasets, DataTracer - -def evaluate_primary_key(solver, metadata, tables): - """Evaluate a primary key solver. - - This computes the accuracy of a primary key solver on the given dataset. It - skips tables without primary keys as well as tables which have composite - primary keys. - - Args: - solver (DataTracer): - A DataTracer instance which implements primary key detection. - metadata (MetaData): - A MetaData instance which describes the tables. - tables (dict): - A dictionary mapping table names to dataframes - - Returns: - dict: - A dictionary mapping metric names to values. +from urllib.parse import urljoin +from urllib.request import urlopen +from zipfile import ZipFile + +import boto3 +import dask +import pandas as pd + +from dask.diagnostics import ProgressBar +from datatracer import DataTracer, load_datasets + +BUCKET_NAME = 'tracer-data' +DATA_URL = 'http://{}.s3.amazonaws.com/'.format(BUCKET_NAME) +DATA_DIR = os.path.expanduser("~/tracer_data") + + +def download(): + """ + This downloads the benchmark datasets from S3. """ + rows = [] + client = boto3.client('s3') + for dataset in client.list_objects(Bucket=BUCKET_NAME)['Contents']: + rows.append(dataset) + dataset_name = dataset['Key'].replace(".zip", "") + dataset_path = os.path.join(DATA_DIR, dataset_name) + if os.path.exists(dataset_path): + dataset["Status"] = "Skipped" + print("Skipping %s" % dataset_name) + else: + dataset["Status"] = "Downloaded" + print("Downloading %s" % dataset_name) + with urlopen(urljoin(DATA_URL, dataset['Key'])) as fp: + with ZipFile(BytesIO(fp.read())) as zipfile: + zipfile.extractall(dataset_path) + return pd.DataFrame(rows) + + +@dask.delayed +def primary_key(solver, target, datasets): + """ + solver - the name of the pipeline? + target - a key in dataset + datasets - map from dataset name to (metadata, tables) + """ + datasets = datasets.copy() + metadata, tables = datasets.pop(target) + + tracer = DataTracer(solver) + tracer.fit(datasets) + y_true = {} for table in metadata.get_tables(): if "primary_key" not in table: @@ -27,10 +60,10 @@ def evaluate_primary_key(solver, metadata, tables): if not isinstance(table["primary_key"], str): continue # Skip tables with composite primary keys y_true[table["name"]] = table["primary_key"] - + correct, total = 0, 0 start = time() - y_pred = solver.solve(tables) + y_pred = tracer.solve(tables) end = time() for table_name, primary_key in y_true.items(): if y_pred.get(table_name) == primary_key: @@ -43,56 +76,203 @@ def evaluate_primary_key(solver, metadata, tables): "inference_time": end - start } -def evaluate_foreign_key(solver, metadata, tables): - """Evaluate a foreign key solver. - This computes the precision, recall, and f1 score of a foreign key solver - on the given dataset. It skips composite foreign primary keys. +def benchmark_primary_key(solver="datatracer.primary_key.basic"): + datasets = load_datasets(DATA_DIR) + dataset_names = list(datasets.keys()) + datasets = dask.delayed(datasets) + dataset_to_metrics = {} + for dataset_name in dataset_names: + dataset_to_metrics[dataset_name] = primary_key( + solver=solver, target=dataset_name, datasets=datasets) - Args: - solver (DataTracer): - A DataTracer instance which implements foreign key detection. - metadata (MetaData): - A MetaData instance which describes the tables. - tables (dict): - A dictionary mapping table names to dataframes + with ProgressBar(): + results = dask.compute(dataset_to_metrics)[0] + for dataset_name, metrics in results.items(): + metrics["dataset"] = dataset_name + return pd.DataFrame(list(results.values())) - Returns: - dict: - A dictionary mapping metric names to values. + +@dask.delayed +def foreign_key(solver, target, datasets): + """ + solver - the name of the pipeline? + target - a key in dataset + datasets - map from dataset name to (metadata, tables) """ + datasets = datasets.copy() + metadata, tables = datasets.pop(target) + + tracer = DataTracer(solver) + tracer.fit(datasets) + y_true = set() for fk in metadata.get_foreign_keys(): if not isinstance(fk["field"], str): continue # Skip composite foreign keys y_true.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) - start = time() - fk_pred = solver.solve(tables) - end = time() + try: + start = time() + fk_pred = tracer.solve(tables) + end = time() - y_pred = set() - for fk in fk_pred: - y_pred.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) + y_pred = set() + for fk in fk_pred: + y_pred.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) - precision = len(y_true.intersection(y_pred)) / len(y_pred) - recall = len(y_true.intersection(y_pred)) / len(y_true) - f1 = 2.0 * precision * recall / (precision + recall) + precision = len(y_true.intersection(y_pred)) / len(y_pred) + recall = len(y_true.intersection(y_pred)) / len(y_true) + f1 = 2.0 * precision * recall / (precision + recall) - return { - "precision": precision, - "recall": recall, - "f1": f1, - "inference_time": end - start - } + return { + "precision": precision, + "recall": recall, + "f1": f1, + "inference_time": end - start + } -if __name__ == "__main__": - datasets = load_datasets("../datatracer/datasets") + except Exception as e: + return { + "error": str(e) + } + + +def benchmark_foreign_key(solver="datatracer.foreign_key.standard"): + datasets = load_datasets(DATA_DIR) + dataset_names = list(datasets.keys()) + datasets = dask.delayed(datasets) + dataset_to_metrics = {} + for dataset_name in dataset_names: + dataset_to_metrics[dataset_name] = foreign_key( + solver=solver, target=dataset_name, datasets=datasets) + + with ProgressBar(): + results = dask.compute(dataset_to_metrics)[0] + for dataset_name, metrics in results.items(): + metrics["dataset"] = dataset_name + return pd.DataFrame(list(results.values())) - solver = DataTracer.load('datatracer.primary_key.basic') - for dataset_name, (metadata, tables) in datasets.items(): - print(dataset_name, evaluate_primary_key(solver, metadata, tables)) - solver = DataTracer.load('datatracer.foreign_key.standard') - for dataset_name, (metadata, tables) in datasets.items(): - print(dataset_name, evaluate_foreign_key(solver, metadata, tables)) +@dask.delayed +def column_map(solver, target, datasets): + """ + solver - the name of the pipeline? + target - a key in dataset + datasets - map from dataset name to (metadata, tables) + """ + datasets = datasets.copy() + metadata, tables = datasets.pop(target) + + tracer = DataTracer(solver) + tracer.fit(datasets) + + list_of_metrics = [] + for constraint in metadata.data["constraints"]: + try: + field = constraint["fields_under_consideration"][0] + related_fields = constraint["related_fields"] + + y_true = set() + for related_field in related_fields: + y_true.add((related_field["table"], related_field["field"])) + + start = time() + y_pred = tracer.solve(tables, target_table=field["table"], target_field=field["field"]) + y_pred = {field for field, score in y_pred.items() if score > 0.0} + end = time() + + precision = len(y_true.intersection(y_pred)) / len(y_pred) + recall = len(y_true.intersection(y_pred)) / len(y_true) + f1 = 2.0 * precision * recall / (precision + recall) + + list_of_metrics.append({ + "table": field["table"], + "field": field["field"], + "precision": precision, + "recall": recall, + "f1": f1, + "inference_time": end - start + }) + + except Exception as e: + list_of_metrics.append({ + "table": field["table"], + "field": field["field"], + "error": str(e) + }) + + return list_of_metrics + + +def benchmark_column_map(solver="datatracer.column_map.basic"): + datasets = load_datasets(DATA_DIR) + dataset_names = list(datasets.keys()) + datasets = dask.delayed(datasets) + dataset_to_metrics = {} + for dataset_name in dataset_names: + dataset_to_metrics[dataset_name] = column_map( + solver=solver, target=dataset_name, datasets=datasets) + + rows = [] + with ProgressBar(): + results = dask.compute(dataset_to_metrics)[0] + for dataset_name, list_of_metrics in results.items(): + for metrics in list_of_metrics: + metrics["dataset"] = dataset_name + rows.append(metrics) + return pd.DataFrame(rows) + + +def _get_parser(): + shared_args = argparse.ArgumentParser(add_help=False) + shared_args.add_argument('-o', '--output', type=str, required=False, help='Path to the CSV file where the report will be written.') + + parser = argparse.ArgumentParser( + prog='datatracer-benchmark', + description='DataTracer Benchmark CLI' + ) + + command = parser.add_subparsers(title='command', help='Command to execute') + parser.set_defaults(benchmark=None) + + subparser = command.add_parser( + 'download', + parents=[shared_args], + help='Download datasets from S3.' + ) + subparser.set_defaults(command=download) + + subparser = command.add_parser( + 'primary', + parents=[shared_args], + help='Primary key benchmark.' + ) + subparser.set_defaults(command=benchmark_primary_key) + + subparser = command.add_parser( + 'foreign', + parents=[shared_args], + help='Foreign key benchmark.' + ) + subparser.set_defaults(command=benchmark_foreign_key) + + subparser = command.add_parser( + 'column', + parents=[shared_args], + help='Column map benchmark.' + ) + subparser.set_defaults(command=benchmark_column_map) + + return parser + +def main(): + parser = _get_parser() + args = parser.parse_args() + df = args.command() + if args.output: + df.to_csv(args.output, index=False) + print(df) + +if __name__ == "__main__": + main() diff --git a/datatracer/data.py b/datatracer/data.py index 35507e6..400acfe 100644 --- a/datatracer/data.py +++ b/datatracer/data.py @@ -6,12 +6,7 @@ """ import os import shutil -from io import BytesIO -from urllib.parse import urljoin -from urllib.request import urlopen -from zipfile import ZipFile -import boto3 import pandas as pd from metad import MetaData @@ -96,25 +91,3 @@ def get_demo_data(path='datatracer_demo', force=False): demo_path = os.path.join(os.path.dirname(__file__), 'datasets') shutil.copytree(demo_path, path) - - -def download(): - BUCKET_NAME = 'tracer-data' # Bucket where the datasets are stored - DATA_URL = 'http://{}.s3.amazonaws.com/'.format(BUCKET_NAME) - - homedir = os.path.expanduser("~/") - if homedir == "~/": - raise ValueError("Could not find a default download directory") - data_dir = os.path.join(homedir, "tracer_data") - - client = boto3.client('s3') - for dataset in client.list_objects(Bucket=BUCKET_NAME)['Contents']: - dataset_name = dataset['Key'].replace(".zip", "") - dataset_path = os.path.join(data_dir, dataset_name) - if os.path.exists(dataset_path): - print("Skipping %s" % dataset_name) - else: - print("Downloading %s" % dataset_name) - with urlopen(urljoin(DATA_URL, dataset['Key'])) as fp: - with ZipFile(BytesIO(fp.read())) as zipfile: - zipfile.extractall(dataset_path) diff --git a/setup.py b/setup.py index 3b7dd33..b9618a6 100644 --- a/setup.py +++ b/setup.py @@ -63,6 +63,9 @@ # Advanced testing 'coverage>=4.5.1,<6', 'tox>=2.9.1,<4', + + # benchmarking + 'dask>=2.15,<3', ] setup( @@ -85,7 +88,8 @@ 'pipelines=datatracer:MLBLOCKS_PIPELINES' ], 'console_scripts': [ - 'datatracer=datatracer.__main__:main' + 'datatracer=datatracer.__main__:main', + 'datatracer-benchmark=benchmark.benchmark:main' ], }, extras_require={ @@ -105,7 +109,7 @@ setup_requires=setup_requires, test_suite='tests', tests_require=tests_require, - url='https://github.com/HDI-Project/DataTracer', + url='https://github.com/data-dev/DataTracer', version='0.0.5.dev0', zip_safe=False, ) From e79ccb682901a574f594eed095fed4bbf0e21e55 Mon Sep 17 00:00:00 2001 From: Kevin Alex Zhang Date: Wed, 10 Jun 2020 01:08:05 -0400 Subject: [PATCH 5/6] Added data directory option --- benchmark/README.md | 6 +- benchmark/benchmark.gif | Bin 0 -> 119681 bytes benchmark/benchmark.py | 223 ++++++++++++------ datatracer/column_map/base.py | 9 +- datatracer/core.py | 12 +- datatracer/foreign_key/base.py | 9 +- datatracer/foreign_key/standard.py | 15 +- ....foreign_key.StandardForeignKeySolver.json | 2 +- ...cer.primary_key.BasicPrimaryKeySolver.json | 2 +- datatracer/primary_key/base.py | 9 +- datatracer/primary_key/basic.py | 14 +- setup.py | 2 +- 12 files changed, 191 insertions(+), 112 deletions(-) create mode 100644 benchmark/benchmark.gif diff --git a/benchmark/README.md b/benchmark/README.md index e51398a..d8a3bd2 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -3,10 +3,14 @@ This directory contains code for benchmarking the performance of `DataTracer` on user-supplied datasets. The datasets for benchmarking can be found in the `s3://tracer-data` bucket. +

+ +

+ Each benchmark - `primary`, `foreign`, and `column` - can be executed by running the following command -> datatracer-benchmark --output /path/to/results.csv +> datatracer-benchmark --csv /path/to/results.csv which will (optionally) generate a CSV file with the benchmark results. diff --git a/benchmark/benchmark.gif b/benchmark/benchmark.gif new file mode 100644 index 0000000000000000000000000000000000000000..578f0a9225d3b3d5b30650189ab999d2bc4a34b4 GIT binary patch literal 119681 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�OuKjjt zMzk367&>Yp=O@a!Q5;F&5d~&Q2DCdeQt9kymRyQ}Lra;5Yq@9X1-91vl-E_I>cP*S z=*dy@)x_PDwz)mc^6)iPNUc$rjR4@-rPLGJP2f7$Oj(vG5GCm7J*AGJ0=t$JBo_sH zwr8phFIk1Nk6729Payd4>rh3FH*Xr(&k#BVk6-(++wcE^toOeKrMiF9ILP0v!D!8W za#OlbSIohWs=~B*Hgcv3pYc2fY_ib=dxf`;&<2d>X@jIDjlz^HK2#MDB4CRUMWQyG z%1pcSWplw^Sm|GfO03)N8*}%cdiC<9#3$V=-WOW&N2jLtn*27rdUBPPLMdT4d(g`V zw?1eCTv_r3=$4z^i0pjj;&%JNg=46IBsNT+1}cNbUpOEBnEKJk)}snJ&WO-y)NyNi z!{O2sr+!T#?fw{l@jDq&$a-QG8}o8zkbC1q*hg5aE!uC$`+~dxHa)#c8&F%hdrB}# zbL^Z{dCcsQjfSyfGF_I6vhac>_>QA}!?Ihx4Ib_OTr2;JPTsdr&_ex`oVO}fE;9c> zW`2#`=Z+4brKrxPf5ygj&Za&7@j7n`CXjb<85k^8q-d{n$;&Jpe*ANKy<~Oh_@7JX zxBQylF8MzC_|N5AQrp8J$q$kIU8zE7%kRZglAp6%?tXvq`1kT3l3(KEcYn@ok$itC z`MvV^?(c7pCBJ`2uMUYUh%E~i%tA=(bUYTiorM`?VOLlJBdIWft}Eie&3@`bUcf zSBi#+#lyD6&w`7eGmA%g#pCV8uSSbsuM~@jC6l%#Q^6(E%#vAN$$WdshtZNxDK93aimfOyHY|( zDU8ikt|}$VWHC&E%Ppl;QkkY*nRZB-ZhDzsRhdCYnbBC8$!eJ?>4dr63CoZZ*6Al~ zB~>TvJ5FpEJK?Z;f<`KLvMYB9DR)gTcdsh<>?rphEB9F~_a#;M*;NFDR0O711Xopr zc2tCoRfMlrFi4e=c9qc~l`-j+u~n6OI;49||BpZdP+d5>9VWo4yox)uj#9*>F=l+7^`X4mlI6a%-@0xhn((ruX|Zt X0Lrs|?GwHpIdbjodcxmCVEz9AP{%zy literal 0 HcmV?d00001 diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index 7aa8873..2b869d9 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -9,25 +9,35 @@ import boto3 import dask import pandas as pd - from dask.diagnostics import ProgressBar + from datatracer import DataTracer, load_datasets BUCKET_NAME = 'tracer-data' DATA_URL = 'http://{}.s3.amazonaws.com/'.format(BUCKET_NAME) -DATA_DIR = os.path.expanduser("~/tracer_data") -def download(): - """ - This downloads the benchmark datasets from S3. +def download(data_dir): + """Download benchmark datasets from S3. + + This downloads the benchmark datasets from S3 into the target folder in an + uncompressed format. It skips datasets that have already been downloaded. + + Args: + data_dir: The directory to download the datasets to. + + Returns: + A DataFrame describing the downloaded datasets. + + Raises: + NoCredentialsError: If AWS S3 credentials are not found. """ rows = [] client = boto3.client('s3') for dataset in client.list_objects(Bucket=BUCKET_NAME)['Contents']: rows.append(dataset) dataset_name = dataset['Key'].replace(".zip", "") - dataset_path = os.path.join(DATA_DIR, dataset_name) + dataset_path = os.path.join(data_dir, dataset_name) if os.path.exists(dataset_path): dataset["Status"] = "Skipped" print("Skipping %s" % dataset_name) @@ -42,10 +52,15 @@ def download(): @dask.delayed def primary_key(solver, target, datasets): - """ - solver - the name of the pipeline? - target - a key in dataset - datasets - map from dataset name to (metadata, tables) + """Benchmark the primary key solver on the target dataset. + + Args: + solver: The name of the primary key pipeline. + target: The name of the target dataset. + datases: A dictionary mapping dataset names to (metadata, tables) tuples. + + Returns: + A dictionary mapping metric names to values. """ datasets = datasets.copy() metadata, tables = datasets.pop(target) @@ -61,6 +76,9 @@ def primary_key(solver, target, datasets): continue # Skip tables with composite primary keys y_true[table["name"]] = table["primary_key"] + if len(y_true) == 0: + return {} # Skip dataset, no primary keys found. + correct, total = 0, 0 start = time() y_pred = tracer.solve(tables) @@ -77,8 +95,20 @@ def primary_key(solver, target, datasets): } -def benchmark_primary_key(solver="datatracer.primary_key.basic"): - datasets = load_datasets(DATA_DIR) +def benchmark_primary_key(data_dir, solver="datatracer.primary_key.basic"): + """Benchmark the primary key solver. + + This uses leave-one-out validation and evaluates the performance of the + solver on the specified datasets. + + Args: + data_dir: The directory containing the datasets. + solver: The name of the primary key pipeline. + + Returns: + A DataFrame containing the benchmark resuls. + """ + datasets = load_datasets(data_dir) dataset_names = list(datasets.keys()) datasets = dask.delayed(datasets) dataset_to_metrics = {} @@ -95,10 +125,15 @@ def benchmark_primary_key(solver="datatracer.primary_key.basic"): @dask.delayed def foreign_key(solver, target, datasets): - """ - solver - the name of the pipeline? - target - a key in dataset - datasets - map from dataset name to (metadata, tables) + """Benchmark the foreign key solver on the target dataset. + + Args: + solver: The name of the foreign key pipeline. + target: The name of the target dataset. + datasets: A dictionary mapping dataset names to (metadata, tables) tuples. + + Returns: + A dictionary mapping metric names to values. """ datasets = datasets.copy() metadata, tables = datasets.pop(target) @@ -112,34 +147,49 @@ def foreign_key(solver, target, datasets): continue # Skip composite foreign keys y_true.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) - try: - start = time() - fk_pred = tracer.solve(tables) - end = time() + start = time() + fk_pred = tracer.solve(tables) + end = time() - y_pred = set() - for fk in fk_pred: - y_pred.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) - - precision = len(y_true.intersection(y_pred)) / len(y_pred) - recall = len(y_true.intersection(y_pred)) / len(y_true) - f1 = 2.0 * precision * recall / (precision + recall) + y_pred = set() + for fk in fk_pred: + y_pred.add((fk["table"], fk["field"], fk["ref_table"], fk["ref_field"])) + if len(y_pred) == 0 or len(y_true) == 0 or \ + len(y_true.intersection(y_pred)) == 0: return { - "precision": precision, - "recall": recall, - "f1": f1, + "precision": 0.0, + "recall": 0.0, + "f1": 0.0, "inference_time": end - start } - except Exception as e: - return { - "error": str(e) - } + precision = len(y_true.intersection(y_pred)) / len(y_pred) + recall = len(y_true.intersection(y_pred)) / len(y_true) + f1 = 2.0 * precision * recall / (precision + recall) + + return { + "precision": precision, + "recall": recall, + "f1": f1, + "inference_time": end - start + } -def benchmark_foreign_key(solver="datatracer.foreign_key.standard"): - datasets = load_datasets(DATA_DIR) +def benchmark_foreign_key(data_dir, solver="datatracer.foreign_key.standard"): + """Benchmark the foreign key solver. + + This uses leave-one-out validation and evaluates the performance of the + solver on the specified datasets. + + Args: + data_dir: The directory containing the datasets. + solver: The name of the foreign key pipeline. + + Returns: + A DataFrame containing the benchmark resuls. + """ + datasets = load_datasets(data_dir) dataset_names = list(datasets.keys()) datasets = dask.delayed(datasets) dataset_to_metrics = {} @@ -156,57 +206,68 @@ def benchmark_foreign_key(solver="datatracer.foreign_key.standard"): @dask.delayed def column_map(solver, target, datasets): - """ - solver - the name of the pipeline? - target - a key in dataset - datasets - map from dataset name to (metadata, tables) + """Benchmark the column map solver on the target dataset. + + Args: + solver: The name of the column map pipeline. + target: The name of the target dataset. + datases: A dictionary mapping dataset names to (metadata, tables) tuples. + + Returns: + A list of dictionaries mapping metric names to values for each deived column. """ datasets = datasets.copy() metadata, tables = datasets.pop(target) + if not metadata.data.get("constraints"): + return {} # Skip dataset, no constraints found. tracer = DataTracer(solver) tracer.fit(datasets) list_of_metrics = [] for constraint in metadata.data["constraints"]: - try: - field = constraint["fields_under_consideration"][0] - related_fields = constraint["related_fields"] - - y_true = set() - for related_field in related_fields: - y_true.add((related_field["table"], related_field["field"])) - - start = time() - y_pred = tracer.solve(tables, target_table=field["table"], target_field=field["field"]) - y_pred = {field for field, score in y_pred.items() if score > 0.0} - end = time() - - precision = len(y_true.intersection(y_pred)) / len(y_pred) - recall = len(y_true.intersection(y_pred)) / len(y_true) - f1 = 2.0 * precision * recall / (precision + recall) - - list_of_metrics.append({ - "table": field["table"], - "field": field["field"], - "precision": precision, - "recall": recall, - "f1": f1, - "inference_time": end - start - }) - - except Exception as e: - list_of_metrics.append({ - "table": field["table"], - "field": field["field"], - "error": str(e) - }) + field = constraint["fields_under_consideration"][0] + related_fields = constraint["related_fields"] + + y_true = set() + for related_field in related_fields: + y_true.add((related_field["table"], related_field["field"])) + + start = time() + y_pred = tracer.solve(tables, target_table=field["table"], target_field=field["field"]) + y_pred = {field for field, score in y_pred.items() if score > 0.0} + end = time() + + precision = len(y_true.intersection(y_pred)) / len(y_pred) + recall = len(y_true.intersection(y_pred)) / len(y_true) + f1 = 2.0 * precision * recall / (precision + recall) + + list_of_metrics.append({ + "table": field["table"], + "field": field["field"], + "precision": precision, + "recall": recall, + "f1": f1, + "inference_time": end - start + }) return list_of_metrics -def benchmark_column_map(solver="datatracer.column_map.basic"): - datasets = load_datasets(DATA_DIR) +def benchmark_column_map(data_dir, solver="datatracer.column_map.basic"): + """Benchmark the column map solver. + + This uses leave-one-out validation and evaluates the performance of the + solver on the specified datasets. + + Args: + data_dir: The directory containing the datasets. + solver: The name of the column map pipeline. + + Returns: + A DataFrame containing the benchmark resuls. + """ + datasets = load_datasets(data_dir) dataset_names = list(datasets.keys()) datasets = dask.delayed(datasets) dataset_to_metrics = {} @@ -226,7 +287,11 @@ def benchmark_column_map(solver="datatracer.column_map.basic"): def _get_parser(): shared_args = argparse.ArgumentParser(add_help=False) - shared_args.add_argument('-o', '--output', type=str, required=False, help='Path to the CSV file where the report will be written.') + shared_args.add_argument('--data_dir', type=str, + default=os.path.expanduser("~/tracer_data"), required=False, + help='Path to the benchmark datasets.') + shared_args.add_argument('--csv', type=str, required=False, + help='Path to the CSV file where the report will be written.') parser = argparse.ArgumentParser( prog='datatracer-benchmark', @@ -266,13 +331,15 @@ def _get_parser(): return parser + def main(): parser = _get_parser() args = parser.parse_args() - df = args.command() - if args.output: - df.to_csv(args.output, index=False) + df = args.command(args.data_dir) + if args.csv: + df.to_csv(args.csv, index=False) print(df) + if __name__ == "__main__": main() diff --git a/datatracer/column_map/base.py b/datatracer/column_map/base.py index 435bbf3..a4d5104 100644 --- a/datatracer/column_map/base.py +++ b/datatracer/column_map/base.py @@ -4,13 +4,14 @@ class ColumnMapSolver: """Base Solver for the data lineage problem of column dependency.""" - def fit(self, list_of_databases): + def fit(self, dict_of_databases): """Fit this solver. Args: - list_of_databases (list): - List of tuples containing ``MetaData`` instnces and table dictinaries, - which contain table names as input and ``pandas.DataFrames`` as values. + dict_of_databases (dict): + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names + as input and ``pandas.DataFrames`` as values. """ pass diff --git a/datatracer/core.py b/datatracer/core.py index 58c2f7b..82fd1db 100644 --- a/datatracer/core.py +++ b/datatracer/core.py @@ -52,15 +52,13 @@ def fit(self, datasets): """Fit the pipeline to the given data. Args: - datasets (list or dict): - List or dict of tuples containing a MetaData instance and a dict - with the tables of the dataset loaded as DataFrames. + datasets (dict): + Dict mapping dataset names to tuples containing a MetaData + instance and a dict with the tables of the dataset loaded + as DataFrames. """ - if isinstance(datasets, dict): - datasets = list(datasets.values()) - self._mlpipeline = self._get_mlpipeline() - self._mlpipeline.fit(list_of_databases=datasets, tables={}) + self._mlpipeline.fit(dict_of_databases=datasets, tables={}) def solve(self, tables, **kwargs): """Solve the data lineage problem. diff --git a/datatracer/foreign_key/base.py b/datatracer/foreign_key/base.py index d31a96e..4019651 100644 --- a/datatracer/foreign_key/base.py +++ b/datatracer/foreign_key/base.py @@ -3,13 +3,14 @@ class ForeignKeySolver(): - def fit(self, list_of_databases): + def fit(self, dict_of_databases): """Fit this solver. Args: - list_of_databases (list): - List of tuples containing ``MetaData`` instnces and table dictinaries, - which contain table names as input and ``pandas.DataFrames`` as values. + dict_of_databases (dict): + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names + as input and ``pandas.DataFrames`` as values. """ pass diff --git a/datatracer/foreign_key/standard.py b/datatracer/foreign_key/standard.py index 6087fb9..73d8519 100644 --- a/datatracer/foreign_key/standard.py +++ b/datatracer/foreign_key/standard.py @@ -1,8 +1,8 @@ from collections import Counter from itertools import permutations -from sklearn.ensemble import RandomForestClassifier from tqdm import tqdm +from sklearn.ensemble import RandomForestClassifier from datatracer.foreign_key.base import ForeignKeySolver @@ -50,16 +50,19 @@ def _feature_vector(self, parent_col, child_col): 1.0 if child_col.dtype == "object" else 0.0, ] - def fit(self, list_of_databases): + def fit(self, dict_of_databases): """Fit this solver. Args: - list_of_databases (list): - List of tuples containing ``MetaData`` instnces and table dictinaries, - which contain table names as input and ``pandas.DataFrames`` as values. + dict_of_databases (dict): + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names + as input and ``pandas.DataFrames`` as values. """ X, y = [], [] - for metadata, tables in tqdm(list_of_databases, "extracting features"): + iterator = tqdm(dict_of_databases.items()) + for database_name, (metadata, tables) in iterator: + iterator.set_description("Extracting features from %s" % database_name) fks = metadata.get_foreign_keys() fks = set([ (fk["table"], fk["field"], fk["ref_table"], fk["ref_field"]) diff --git a/datatracer/jsons/primitives/datatracer.foreign_key.StandardForeignKeySolver.json b/datatracer/jsons/primitives/datatracer.foreign_key.StandardForeignKeySolver.json index 51d9518..87ae0a7 100644 --- a/datatracer/jsons/primitives/datatracer.foreign_key.StandardForeignKeySolver.json +++ b/datatracer/jsons/primitives/datatracer.foreign_key.StandardForeignKeySolver.json @@ -6,7 +6,7 @@ "method": "fit", "args": [ { - "name": "list_of_databases", + "name": "dict_of_databases", "type": "list" } ] diff --git a/datatracer/jsons/primitives/datatracer.primary_key.BasicPrimaryKeySolver.json b/datatracer/jsons/primitives/datatracer.primary_key.BasicPrimaryKeySolver.json index ff81725..c387f9f 100644 --- a/datatracer/jsons/primitives/datatracer.primary_key.BasicPrimaryKeySolver.json +++ b/datatracer/jsons/primitives/datatracer.primary_key.BasicPrimaryKeySolver.json @@ -6,7 +6,7 @@ "method": "fit", "args": [ { - "name": "list_of_databases", + "name": "dict_of_databases", "type": "list" } ] diff --git a/datatracer/primary_key/base.py b/datatracer/primary_key/base.py index f033f8f..d03c359 100644 --- a/datatracer/primary_key/base.py +++ b/datatracer/primary_key/base.py @@ -3,13 +3,14 @@ class PrimaryKeySolver(): - def fit(self, list_of_databases): + def fit(self, dict_of_databases): """Fit this solver. Args: - list_of_databases (list): - List of tuples containing ``MetaData`` instnces and table dictinaries, - which contain table names as input and ``pandas.DataFrames`` as values. + dict_of_databases (dict): + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names + as input and ``pandas.DataFrames`` as values. """ pass diff --git a/datatracer/primary_key/basic.py b/datatracer/primary_key/basic.py index 9280cd6..be9064f 100644 --- a/datatracer/primary_key/basic.py +++ b/datatracer/primary_key/basic.py @@ -1,6 +1,7 @@ """Basic Primary Key Solver module.""" import numpy as np +from tqdm import tqdm from sklearn.ensemble import RandomForestClassifier from datatracer.primary_key.base import PrimaryKeySolver @@ -27,16 +28,19 @@ def _feature_vector(self, table, column_name): 1.0 if column.dtype == "object" else 0.0, ] - def fit(self, list_of_databases): + def fit(self, dict_of_databases): """Fit this solver. Args: - list_of_databases (list): - List of tuples containing ``MetaData`` instnces and table dictinaries, - which contain table names as input and ``pandas.DataFrames`` as values. + dict_of_databases (dict): + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names + as input and ``pandas.DataFrames`` as values. """ X, y = [], [] - for metadata, tables in list_of_databases: + iterator = tqdm(dict_of_databases.items()) + for database_name, (metadata, tables) in iterator: + iterator.set_description("Extracting features from %s" % database_name) for table in metadata.get_tables(): if "primary_key" not in table: continue diff --git a/setup.py b/setup.py index b9618a6..1331ef5 100644 --- a/setup.py +++ b/setup.py @@ -105,7 +105,7 @@ keywords='datatracer data-tracer Data Tracer', name='datatracer', packages=find_packages(include=['datatracer', 'datatracer.*']), - python_requires='>=3.5,<=3.8', + python_requires='>=3.5', setup_requires=setup_requires, test_suite='tests', tests_require=tests_require, From 5a251dedec1a5eabaecc45a2161a8b48f294f259 Mon Sep 17 00:00:00 2001 From: Kevin Alex Zhang Date: Thu, 18 Jun 2020 21:18:36 -0400 Subject: [PATCH 6/6] Addressed feedback --- datatracer/column_map/base.py | 5 +- datatracer/core.py | 4 +- datatracer/foreign_key/base.py | 5 +- datatracer/foreign_key/standard.py | 2 +- datatracer/primary_key/base.py | 5 +- datatracer/primary_key/basic.py | 6 +- setup.py | 1 + tutorials/DataTracer Quickstart.ipynb | 83 +++++++++++++------------- tutorials/Introducing DataTracer.ipynb | 14 ++--- 9 files changed, 62 insertions(+), 63 deletions(-) diff --git a/datatracer/column_map/base.py b/datatracer/column_map/base.py index a4d5104..a257ce5 100644 --- a/datatracer/column_map/base.py +++ b/datatracer/column_map/base.py @@ -9,11 +9,10 @@ def fit(self, dict_of_databases): Args: dict_of_databases (dict): - Map from database names to tuples containing ``MetaData`` - instances and table dictionaries, which contain table names + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names as input and ``pandas.DataFrames`` as values. """ - pass def solve(self, tables, foreign_keys, target_table, target_field): """Find the fields which contributed to the target_field the most. diff --git a/datatracer/core.py b/datatracer/core.py index 82fd1db..fcc4104 100644 --- a/datatracer/core.py +++ b/datatracer/core.py @@ -53,8 +53,8 @@ def fit(self, datasets): Args: datasets (dict): - Dict mapping dataset names to tuples containing a MetaData - instance and a dict with the tables of the dataset loaded + Dict mapping dataset names to tuples containing a MetaData + instance and a dict with the tables of the dataset loaded as DataFrames. """ self._mlpipeline = self._get_mlpipeline() diff --git a/datatracer/foreign_key/base.py b/datatracer/foreign_key/base.py index 4019651..82a336e 100644 --- a/datatracer/foreign_key/base.py +++ b/datatracer/foreign_key/base.py @@ -8,11 +8,10 @@ def fit(self, dict_of_databases): Args: dict_of_databases (dict): - Map from database names to tuples containing ``MetaData`` - instances and table dictionaries, which contain table names + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names as input and ``pandas.DataFrames`` as values. """ - pass def solve(self, tables, primary_keys=None): """Solve the foreign key detection problem. diff --git a/datatracer/foreign_key/standard.py b/datatracer/foreign_key/standard.py index 2ae949c..adb5690 100644 --- a/datatracer/foreign_key/standard.py +++ b/datatracer/foreign_key/standard.py @@ -1,8 +1,8 @@ from collections import Counter from itertools import permutations -from tqdm import tqdm from sklearn.ensemble import RandomForestClassifier +from tqdm import tqdm from datatracer.foreign_key.base import ForeignKeySolver diff --git a/datatracer/primary_key/base.py b/datatracer/primary_key/base.py index d03c359..df316d2 100644 --- a/datatracer/primary_key/base.py +++ b/datatracer/primary_key/base.py @@ -8,11 +8,10 @@ def fit(self, dict_of_databases): Args: dict_of_databases (dict): - Map from database names to tuples containing ``MetaData`` - instances and table dictionaries, which contain table names + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names as input and ``pandas.DataFrames`` as values. """ - pass def solve(self, tables): """Solve the primary key detection problem. diff --git a/datatracer/primary_key/basic.py b/datatracer/primary_key/basic.py index d7c14a7..b0c657a 100644 --- a/datatracer/primary_key/basic.py +++ b/datatracer/primary_key/basic.py @@ -1,8 +1,8 @@ """Basic Primary Key Solver module.""" import numpy as np -from tqdm import tqdm from sklearn.ensemble import RandomForestClassifier +from tqdm import tqdm from datatracer.primary_key.base import PrimaryKeySolver @@ -33,8 +33,8 @@ def fit(self, dict_of_databases): Args: dict_of_databases (dict): - Map from database names to tuples containing ``MetaData`` - instances and table dictionaries, which contain table names + Map from database names to tuples containing ``MetaData`` + instances and table dictionaries, which contain table names as input and ``pandas.DataFrames`` as values. """ X, y = [], [] diff --git a/setup.py b/setup.py index 2014830..9385f01 100644 --- a/setup.py +++ b/setup.py @@ -66,6 +66,7 @@ # benchmarking 'dask>=2.15,<3', + 'distributed>=2.15,<3', ] setup( diff --git a/tutorials/DataTracer Quickstart.ipynb b/tutorials/DataTracer Quickstart.ipynb index 26a1840..fb5037f 100644 --- a/tutorials/DataTracer Quickstart.ipynb +++ b/tutorials/DataTracer Quickstart.ipynb @@ -33,7 +33,9 @@ { "cell_type": "code", "execution_count": 1, - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "name": "stdout", @@ -83,7 +85,7 @@ { "data": { "text/plain": [ - "dict_keys(['mutagenesis', 'Chess', 'classicmodels', 'university', 'Bupa', 'trains', 'SameGen', 'NBA', 'pubs'])" + "dict_keys(['posts', 'NBA', 'university', 'pubs', 'Chess', 'classicmodels', 'mutagenesis', 'Bupa', 'trains', 'SameGen'])" ] }, "execution_count": 3, @@ -395,15 +397,6 @@ "the dataset that we just explored, using the rest of the datasets as our training data." ] }, - { - "cell_type": "code", - "execution_count": 10, - "metadata": {}, - "outputs": [], - "source": [ - "training_datasets = list(datasets.values())" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -423,7 +416,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 10, "metadata": {}, "outputs": [ { @@ -435,7 +428,7 @@ " 'datatracer.primary_key.basic']" ] }, - "execution_count": 11, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } @@ -467,7 +460,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ @@ -486,11 +479,19 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Extracting features from SameGen: 100%|██████████| 9/9 [00:00<00:00, 145.54it/s]\n" + ] + } + ], "source": [ - "dtr.fit(training_datasets)" + "dtr.fit(datasets)" ] }, { @@ -503,7 +504,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": {}, "outputs": [], "source": [ @@ -519,7 +520,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": {}, "outputs": [ { @@ -535,7 +536,7 @@ " 'products': 'productCode'}" ] }, - "execution_count": 15, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -557,14 +558,14 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "extracting features: 100%|██████████| 8/8 [00:00<00:00, 19.07it/s]\n" + "Extracting features from SameGen: 100%|██████████| 9/9 [00:01<00:00, 8.46it/s] \n" ] } ], @@ -572,7 +573,7 @@ "from datatracer import DataTracer\n", "\n", "dtr = DataTracer('datatracer.foreign_key.standard')\n", - "dtr.fit(training_datasets)\n", + "dtr.fit(datasets)\n", "foreign_keys = dtr.solve(tables)" ] }, @@ -586,7 +587,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -618,7 +619,7 @@ " 'ref_field': 'officeCode'}]" ] }, - "execution_count": 17, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -649,14 +650,14 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "extracting features: 100%|██████████| 8/8 [00:00<00:00, 18.00it/s]\n" + "Extracting features from SameGen: 100%|██████████| 9/9 [00:01<00:00, 8.10it/s] \n" ] } ], @@ -664,7 +665,7 @@ "from datatracer import DataTracer\n", "\n", "dtr = DataTracer('datatracer.column_map.basic')\n", - "dtr.fit(training_datasets)\n", + "dtr.fit(datasets)\n", "column_map = dtr.solve(\n", " tables,\n", " target_table='orderdetails',\n", @@ -682,18 +683,18 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{('orderdetails', 'orderNumber'): 0.3854540808399761,\n", - " ('orderdetails', 'priceEach'): 0.43313416486749556,\n", - " ('orderdetails', 'orderLineNumber'): 0.18141175429252837}" + "{('orderdetails', 'orderNumber'): 0.3831749750863929,\n", + " ('orderdetails', 'priceEach'): 0.4397562935716433,\n", + " ('orderdetails', 'orderLineNumber'): 0.1770687313419638}" ] }, - "execution_count": 19, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -719,7 +720,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -735,7 +736,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -748,19 +749,19 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{('orderdetails', 'orderNumber'): 0.00022471076974350297,\n", - " ('orderdetails', 'priceEach'): 0.00015177917546761553,\n", - " ('orderdetails', 'orderLineNumber'): 0.00014925873321498974,\n", - " ('orderdetails', 'quantityOrdered_x2'): 0.9994742513215739}" + "{('orderdetails', 'orderNumber'): 0.00019290156505023432,\n", + " ('orderdetails', 'priceEach'): 0.00014624354192835704,\n", + " ('orderdetails', 'orderLineNumber'): 9.105599151739842e-05,\n", + " ('orderdetails', 'quantityOrdered_x2'): 0.9995697989015039}" ] }, - "execution_count": 22, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } diff --git a/tutorials/Introducing DataTracer.ipynb b/tutorials/Introducing DataTracer.ipynb index 9555b19..2598548 100644 --- a/tutorials/Introducing DataTracer.ipynb +++ b/tutorials/Introducing DataTracer.ipynb @@ -338,12 +338,12 @@ { "data": { "text/plain": [ - "{('users', 'id'): 0.0,\n", - " ('users', 'birthyear'): 0.9999980097244106,\n", - " ('users', 'height'): 5.180246597799326e-07,\n", + "{('users', 'id'): 5.046102180418218e-07,\n", + " ('users', 'birthyear'): 0.9999978458447893,\n", + " ('users', 'height'): 0.0,\n", " ('users', 'nb_posts'): 0.0,\n", - " ('posts', 'uid'): 6.40004263964262e-07,\n", - " ('posts', 'id'): 2.8343526963126543e-07}" + " ('posts', 'uid'): 0.0,\n", + " ('posts', 'id'): 7.225373384175289e-07}" ] }, "execution_count": 7, @@ -383,8 +383,8 @@ " ('users', 'age'): 0.0,\n", " ('users', 'birthyear'): 0.0,\n", " ('users', 'height'): 0.0,\n", - " ('posts', 'uid'): 0.4785790560832036,\n", - " ('posts', 'id'): 0.5136442420361127}" + " ('posts', 'uid'): 0.5263822157542845,\n", + " ('posts', 'id'): 0.46999603701224835}" ] }, "execution_count": 8,