Repository: GlobalMaksimum/sadedegel (language: python)

Issue:
Implement Hotel Sentiment Dataset
[This paper](https://journals.tubitak.gov.tr/elektrik/issues/elk-19-27-3/elk-27-3-16-1808-189.pdf) mentioned in #227 uses a hybrid(ensemble) model instead of a hybrid sentiment dataset. Since movie and tweet sentiment datasets are already in sadedegel, I will implement Hotel Sentiment Dataset, added to bucket by @dafajon,  from [HUMIR dataset](http://humirapps.cs.hacettepe.edu.tr/tsad.aspx) mentioned in the paper above.


Discussion / hints:

Fix patch:
diff --git a/sadedegel/dataset/README.md b/sadedegel/dataset/README.md
index 49808518..f7496f70 100644
--- a/sadedegel/dataset/README.md
+++ b/sadedegel/dataset/README.md
@@ -295,6 +295,26 @@ test = load_movie_sentiment_test()
 test_label = load_movie_sentiment_test_label()
 ```
 
+## `hotel_sentiment`
+
+Hotel sentiment data is part of [HUMIR dataset](http://humirapps.cs.hacettepe.edu.tr/tsad.aspx), which is a combination
+of hotel and movie reviews. This implementation contains reviews of type 'Hotel Review'.
+
+It contains 11,600 instances with `POSITIVE` and `NEGATIVE` sentiments as a class label. The train and
+test split is based on the split present in HUMIR dataset.
+
+### Using Corpus
+
+```python
+from sadedegel.dataset.hotel_sentiment import load_hotel_sentiment_train
+from sadedegel.dataset.hotel_sentiment import load_hotel_sentiment_test
+from sadedegel.dataset.hotel_sentiment import load_hotel_sentiment_test_label
+
+train = load_hotel_sentiment_train()
+test = load_hotel_sentiment_test()
+test_label = load_hotel_sentiment_test_label()
+```
+
 [scraper]: https://github.com/GlobalMaksimum/sadedegel-scraper
 
 [Taner Sezer]: https://github.com/tanerim
diff --git a/sadedegel/dataset/hotel_sentiment/__init__.py b/sadedegel/dataset/hotel_sentiment/__init__.py
new file mode 100644
index 00000000..b73d9560
--- /dev/null
+++ b/sadedegel/dataset/hotel_sentiment/__init__.py
@@ -0,0 +1,2 @@
+from ._core import load_hotel_sentiment_train, load_hotel_sentiment_test, load_hotel_sentiment_test_label
+from ._core import CORPUS_SIZE, CLASS_VALUES
diff --git a/sadedegel/dataset/hotel_sentiment/__main__.py b/sadedegel/dataset/hotel_sentiment/__main__.py
new file mode 100644
index 00000000..140cd0ab
--- /dev/null
+++ b/sadedegel/dataset/hotel_sentiment/__main__.py
@@ -0,0 +1,83 @@
+import os.path
+from pathlib import Path
+import sys
+from itertools import tee
+from smart_open import open
+import click
+import boto3
+from loguru import logger
+from ._core import load_hotel_sentiment_train, load_hotel_sentiment_test, load_hotel_sentiment_test_label, \
+    CLASS_VALUES, CORPUS_SIZE
+from zipfile import ZipFile
+from rich.console import Console
+
+console = Console()
+
+logger.disable("sadedegel")
+
+
+@click.group(help="Hotel Sentiment Dataset Commandline")
+def cli():
+    pass
+
+
+@cli.command()
+@click.option("--access-key", help="Access Key ID to download dataset.", prompt=True,
+              default=lambda: os.environ.get('sadedegel_access_key', ''))
+@click.option("--secret-key", help="Secret Key ID to download dataset.", prompt=True,
+              default=lambda: os.environ.get('sadedegel_secret_key', ''))
+@click.option("--data_home", '-d', help="Data home directory", default="~/.sadedegel_data")
+def download(access_key, secret_key, data_home):
+    """Download tokenization corpus from cloud with your key."""
+
+    data_home = Path(os.path.expanduser(data_home))
+    data_home.mkdir(parents=True, exist_ok=True)
+    console.print(f"Data directory for  data {data_home}")
+
+    transport_params = {
+        'session': boto3.Session(aws_access_key_id=access_key,
+                                 aws_secret_access_key=secret_key),
+        'resource_kwargs': {
+            'endpoint_url': 'https://storage.googleapis.com',
+        }
+    }
+
+    url = f"s3://sadedegel/dataset/hotel_sentiment.zip"
+
+    with open(url, 'rb', transport_params=transport_params) as fp:
+        with ZipFile(fp) as zp:
+            zp.extractall(data_home)
+
+
+@cli.command()
+def validate():
+    """Sanity check on corpus
+    """
+    with console.status("[bold yellow]Validating train"):
+        train = load_hotel_sentiment_train()
+
+        train_clone, train = tee(train, 2)
+
+        n_train = sum(1 for _ in train_clone)
+
+        if n_train == CORPUS_SIZE:
+            console.log("Cardinality check [yellow]DONE[/yellow]")
+        else:
+            console.log("Cardinality check [red]FAILED[/red]")
+            console.log(f"|Hotel Sentiment (train)| : {n_train}")
+            sys.exit(1)
+
+    with console.status("[bold yellow]Validate test"):
+        test = set((d['id'] for d in load_hotel_sentiment_test()))
+        test_label = set((d['id'] for d in load_hotel_sentiment_test_label()))
+
+        a_b, ab, b_a = test - test_label, test & test_label, test_label - test
+
+        if len(a_b) == 0 and len(b_a) == 0:
+            console.log("Corpus check [green]DONE[/green]")
+        else:
+            console.log(f"Test file [red]DIVERGE[/red] from label file. {len(a_b)}, {len(b_a)}")
+
+
+if __name__ == "__main__":
+    cli()
diff --git a/sadedegel/dataset/hotel_sentiment/_core.py b/sadedegel/dataset/hotel_sentiment/_core.py
new file mode 100644
index 00000000..d738842f
--- /dev/null
+++ b/sadedegel/dataset/hotel_sentiment/_core.py
@@ -0,0 +1,79 @@
+import csv
+import gzip
+from pathlib import Path
+from rich.console import Console
+
+CLASS_VALUES = ["NEGATIVE", "POSITIVE"]
+CORPUS_SIZE = 5800
+
+console = Console()
+
+__general_download_message__ = """Ensure that you have properly downloaded hotel sentiment corpus using
+
+            python -m sadedegel.dataset.hotel_sentiment download --access-key xxx --secret-key xxxx
+
+        Unfortunately due to data licensing issues we could not share data publicly. 
+        Get in touch with sadedegel team to obtain a download key.
+        """
+
+
+def check_directory_structure(path: str) -> bool:
+    base_dir = Path(path).expanduser()
+
+    hotel_sentiment_dir = base_dir / 'hotel_sentiment'
+
+    if not base_dir.exists():
+        console.log(f"Dataset base directory ([bold red]{base_dir}[/bold red]) does not exist")
+
+    elif not hotel_sentiment_dir.exists():
+        console.log(
+            f" directory ([bold red]{hotel_sentiment_dir}[/bold red]) does not exist")
+
+    else:
+        return True
+
+    console.log(__general_download_message__)
+
+    return False
+
+
+def load_hotel_sentiment_train(data_home="~/.sadedegel_data"):
+    if not check_directory_structure(data_home):
+        raise Exception(" validation error")
+
+    train_csv = Path(data_home).expanduser() / "hotel_sentiment"
+    train_csv = train_csv / "hotel_sentiment_train.csv.gz"
+
+    with gzip.open(train_csv, "rt") as csvfile:
+        rd = csv.DictReader(csvfile)
+
+        for rec in rd:
+            yield dict(id=rec['text_uuid'], text=rec['text'], sentiment_class=int(rec['sentiment']))
+
+
+def load_hotel_sentiment_test(data_home="~/.sadedegel_data"):
+    if not check_directory_structure(data_home):
+        raise Exception("Hotel Sentiment Corpus validation error")
+
+    test_csv = Path(data_home).expanduser() / "hotel_sentiment"
+    test_csv = test_csv / "hotel_sentiment_test.csv.gz"
+
+    with gzip.open(test_csv, "rt") as csvfile:
+        rd = csv.DictReader(csvfile)
+
+        for rec in rd:
+            yield dict(id=rec['text_uuid'], text=rec['text'])
+
+
+def load_hotel_sentiment_test_label(data_home="~/.sadedegel_data"):
+    if not check_directory_structure(data_home):
+        raise Exception("Hotel Sentiment Corpus validation error")
+
+    test_csv = Path(data_home).expanduser() / "hotel_sentiment"
+    test_csv = test_csv / "hotel_sentiment_target.csv.gz"
+
+    with gzip.open(test_csv, "rt") as csvfile:
+        rd = csv.DictReader(csvfile)
+
+        for rec in rd:
+            yield dict(id=rec['text_uuid'], sentiment_class=int(rec['sentiment']))


Test patch:
diff --git a/tests/datasets/context.py b/tests/datasets/context.py
index 839954eb..6a8e12a3 100644
--- a/tests/datasets/context.py
+++ b/tests/datasets/context.py
@@ -12,6 +12,7 @@
 from sadedegel.dataset.telco_sentiment import load_telco_sentiment_train, load_telco_sentiment_test, load_telco_sentiment_test_label # noqa # pylint: disable=unused-import, wrong-import-position
 from sadedegel.dataset.telco_sentiment import CLASS_VALUES as TELCO_CLASS_VALUES # noqa # pylint: disable=unused-import, wrong-import-position
 from sadedegel.dataset import movie_sentiment # noqa # pylint: disable=unused-import, wrong-import-position
+from sadedegel.dataset import hotel_sentiment # noqa # pylint: disable=unused-import, wrong-import-position
 from sadedegel.bblock.cli.__main__ import tok_eval # noqa # pylint: disable=unused-import, wrong-import-position
 from sadedegel.dataset import util # noqa # pylint: disable=unused-import, wrong-import-position
 from sadedegel.dataset import file_paths, CorpusTypeEnum # noqa # pylint: disable=unused-import, wrong-import-position
diff --git a/tests/datasets/test_hotel_sentiment.py b/tests/datasets/test_hotel_sentiment.py
new file mode 100644
index 00000000..e8b81a09
--- /dev/null
+++ b/tests/datasets/test_hotel_sentiment.py
@@ -0,0 +1,35 @@
+from pathlib import Path  # pylint: disable=unused-import
+from os.path import expanduser  # pylint: disable=unused-import
+
+import pytest
+
+from .context import hotel_sentiment
+
+
+@pytest.mark.skipif('not Path(expanduser("~/.sadedegel_data")).exists()')
+def test_data_load():
+    data = hotel_sentiment.load_hotel_sentiment_train()
+    for i, row in enumerate(data):
+        assert any(key in row.keys() for key in ['id', 'text', 'sentiment_class'])
+        assert isinstance(row['id'], str)
+        assert isinstance(row['text'], str)
+        assert hotel_sentiment.CLASS_VALUES[row['sentiment_class']] in ['POSITIVE', 'NEGATIVE']
+    assert i + 1 == 5800
+
+@pytest.mark.skipif('not Path(expanduser("~/.sadedegel_data")).exists()')
+def test_data_load_test():
+    data = hotel_sentiment.load_hotel_sentiment_test()
+    for i, row in enumerate(data):
+        assert any(key in row.keys() for key in ['id', 'text'])
+        assert isinstance(row['id'], str)
+        assert isinstance(row['text'], str)
+    assert i +1 == 5800
+
+@pytest.mark.skipif('not Path(expanduser("~/.sadedegel_data")).exists()')
+def test_data_load_label():
+    data = hotel_sentiment.load_hotel_sentiment_test_label()
+    for i, row in enumerate(data):
+        assert any(key in row.keys() for key in ['id', 'sentiment_class'])
+        assert isinstance(row['id'], str)
+        assert hotel_sentiment.CLASS_VALUES[row['sentiment_class']] in ['POSITIVE', 'NEGATIVE']
+    assert i +1 == 5800