Repository: remix/partridge

Issue:
Non-numeric IDs
* partridge version: 1.1.1 (via pip)
* Python version: 3.9
* Operating System: Windows

### Description

As is, it appears that any non-numeric ID field will crash Partridge because Pandas is expecting IDs to be integers. Unfortunately the [GTFS spec](https://gtfs.org/reference/static/#field-types) states that a valid ID "is a sequence of any UTF-8 characters", and in practice the use of non-numeric IDs is widespread.

### What I Did

```
import partridge
partridge.read_busiest_date("gtfs.zip")
# For example - issue is not limited to this function

Traceback (most recent call last):
  File "pandas\_libs\parsers.pyx", line 1050, in pandas._libs.parsers.TextReader._convert_tokens
TypeError: Cannot cast array data from dtype('O') to dtype('int32') according to the rule 'safe'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<pyshell#9>", line 1, in <module>
    partridge.read_busiest_date("gtfs.zip")
  File "...\partridge\readers.py", line 60, in read_busiest_date
    return _busiest_date(feed)
  File "...\partridge\readers.py", line 118, in _busiest_date
    service_ids_by_date = _service_ids_by_date(feed)
  File "...\partridge\readers.py", line 156, in _service_ids_by_date
    service_ids = set(feed.trips.service_id)
  File "...\partridge\gtfs.py", line 16, in getter
    return self.get(filename)
  File "...\partridge\gtfs.py", line 48, in get
    df = self._read(filename)
  File "...\partridge\gtfs.py", line 48, in get
    df = self._read(filename)
  File "...\partridge\gtfs.py", line 102, in _read_csv
    df = pd.read_csv(path, dtype=np.unicode, encoding=encoding, index_col=False)
  File "...\pandas\io\parsers.py", line 605, in read_csv
    return _read(filepath_or_buffer, kwds)
  File "...\pandas\io\parsers.py", line 463, in _read
    return parser.read(nrows)
  File "...\pandas\io\parsers.py", line 1052, in read
    index, columns, col_dict = self._engine.read(nrows)
  File "...\pandas\io\parsers.py", line 2056, in read
    data = self._reader.read(nrows)
  File "pandas\_libs\parsers.pyx", line 756, in pandas._libs.parsers.TextReader.read
  File "pandas\_libs\parsers.pyx", line 771, in pandas._libs.parsers.TextReader._read_low_memory
  File "pandas\_libs\parsers.pyx", line 850, in pandas._libs.parsers.TextReader._read_rows
  File "pandas\_libs\parsers.pyx", line 982, in pandas._libs.parsers.TextReader._convert_column_data
  File "pandas\_libs\parsers.pyx", line 1056, in pandas._libs.parsers.TextReader._convert_tokens
ValueError: invalid literal for int() with base 10: 'offending_string_id'
```


Discussion:

Patch:
diff --git a/partridge/gtfs.py b/partridge/gtfs.py
index 62b91c5..cc5e171 100644
--- a/partridge/gtfs.py
+++ b/partridge/gtfs.py
@@ -3,7 +3,6 @@ from threading import RLock
 from typing import Dict, Optional, Union
 
 import networkx as nx
-import numpy as np
 import pandas as pd
 
 from .config import default_config
@@ -99,7 +98,7 @@ class Feed(object):
         with open(path, "rb") as f:
             encoding = detect_encoding(f)
 
-        df = pd.read_csv(path, dtype=np.unicode, encoding=encoding, index_col=False)
+        df = pd.read_csv(path, dtype=str, encoding=encoding, index_col=False)
 
         # Strip leading/trailing whitespace from column names
         df.rename(columns=lambda x: x.strip(), inplace=True)
diff --git a/partridge/utilities.py b/partridge/utilities.py
index f9419ef..6cc941f 100644
--- a/partridge/utilities.py
+++ b/partridge/utilities.py
@@ -2,7 +2,6 @@ from typing import Any, Dict, Iterable, Optional, Set, BinaryIO, Union
 
 from cchardet import UniversalDetector
 import networkx as nx
-import numpy as np
 import pandas as pd
 from pandas.core.common import flatten
 
@@ -67,4 +66,4 @@ def detect_encoding(f: BinaryIO, limit: int = 2500) -> str:
 def empty_df(columns: Optional[Iterable[str]] = None) -> pd.DataFrame:
     columns = [] if columns is None else columns
     empty: Dict = {col: [] for col in columns}
-    return pd.DataFrame(empty, columns=columns, dtype=np.unicode)
+    return pd.DataFrame(empty, columns=columns, dtype=str)


Test patch:
diff --git a/tests/test_readers.py b/tests/test_readers.py
index 1ae03f3..b6a9acb 100644
--- a/tests/test_readers.py
+++ b/tests/test_readers.py
@@ -9,7 +9,7 @@ from .helpers import fixture, zip_file
 
 def test_load_feed():
     feed = ptg.load_feed(fixture("amazon-2017-08-06"))
-    assert feed.stop_times.dtypes["stop_id"] == np.object
+    assert feed.stop_times.dtypes["stop_id"] == object
     assert feed.stop_times.dtypes["stop_sequence"] == np.int64
     assert feed.stop_times.dtypes["arrival_time"] == np.float64
 
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
index 9adf434..f828d2a 100644
--- a/tests/test_utilities.py
+++ b/tests/test_utilities.py
@@ -2,7 +2,6 @@ import io
 import networkx as nx
 import pytest
 
-import numpy as np
 import pandas as pd
 from partridge.utilities import (
     detect_encoding,
@@ -44,7 +43,7 @@ def test_empty_df():
     actual = empty_df(["foo", "bar"])
 
     expected = pd.DataFrame(
-        {"foo": [], "bar": []}, columns=["foo", "bar"], dtype=np.unicode
+        {"foo": [], "bar": []}, columns=["foo", "bar"], dtype=str
     )
 
     assert actual.equals(expected)