dataiter 0.999__tar.gz → 1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {dataiter-0.999 → dataiter-1.0}/COPYING +1 -1
  2. {dataiter-0.999 → dataiter-1.0}/PKG-INFO +2 -2
  3. {dataiter-0.999 → dataiter-1.0}/dataiter/__init__.py +2 -1
  4. {dataiter-0.999 → dataiter-1.0}/dataiter/data_frame.py +58 -28
  5. {dataiter-0.999 → dataiter-1.0}/dataiter/dt.py +0 -5
  6. {dataiter-0.999 → dataiter-1.0}/dataiter/dtypes.py +6 -0
  7. dataiter-1.0/dataiter/regex.py +148 -0
  8. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_data_frame.py +66 -3
  9. dataiter-1.0/dataiter/test/test_regex.py +92 -0
  10. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_vector.py +16 -0
  11. {dataiter-0.999 → dataiter-1.0}/dataiter/vector.py +144 -3
  12. {dataiter-0.999 → dataiter-1.0}/pyproject.toml +2 -2
  13. {dataiter-0.999 → dataiter-1.0}/.gitignore +0 -0
  14. {dataiter-0.999 → dataiter-1.0}/AUTHORS.md +0 -0
  15. {dataiter-0.999 → dataiter-1.0}/README.md +0 -0
  16. {dataiter-0.999 → dataiter-1.0}/dataiter/aggregate.py +0 -0
  17. {dataiter-0.999 → dataiter-1.0}/dataiter/deco.py +0 -0
  18. {dataiter-0.999 → dataiter-1.0}/dataiter/geojson.py +0 -0
  19. {dataiter-0.999 → dataiter-1.0}/dataiter/io.py +0 -0
  20. {dataiter-0.999 → dataiter-1.0}/dataiter/list_of_dicts.py +0 -0
  21. {dataiter-0.999 → dataiter-1.0}/dataiter/test/__init__.py +0 -0
  22. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_aggregate.py +0 -0
  23. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_dt.py +0 -0
  24. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_geojson.py +0 -0
  25. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_io.py +0 -0
  26. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_list_of_dicts.py +0 -0
  27. {dataiter-0.999 → dataiter-1.0}/dataiter/test/test_util.py +0 -0
  28. {dataiter-0.999 → dataiter-1.0}/dataiter/util.py +0 -0
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019-2024 Osmo Salomaa
3
+ Copyright (c) 2019-2025 Osmo Salomaa
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dataiter
3
- Version: 0.999
3
+ Version: 1.0
4
4
  Summary: Simple, light-weight data frames for Python
5
5
  Project-URL: Homepage, https://github.com/otsaloma/dataiter
6
6
  Author-email: Osmo Salomaa <otsaloma@iki.fi>
@@ -10,7 +10,7 @@ License-File: COPYING
10
10
  Requires-Python: >=3.9.0
11
11
  Requires-Dist: attd>=0.3
12
12
  Requires-Dist: numpy<3.0,>=2.0
13
- Requires-Dist: pandas>=1.0
13
+ Requires-Dist: pyarrow>=2.0
14
14
  Requires-Dist: wcwidth>=0.1
15
15
  Description-Content-Type: text/markdown
16
16
 
@@ -25,7 +25,7 @@ import numpy as np
25
25
 
26
26
  from dataiter import util
27
27
 
28
- __version__ = "0.999"
28
+ __version__ = "1.0"
29
29
 
30
30
  DEFAULT_PEEK_ELEMENTS = 10
31
31
  DEFAULT_PEEK_ITEMS = 3
@@ -94,6 +94,7 @@ from dataiter.data_frame import DataFrameColumn # noqa
94
94
  from dataiter.geojson import GeoJSON # noqa
95
95
  from dataiter.list_of_dicts import ListOfDicts # noqa
96
96
  from dataiter import dt # noqa
97
+ from dataiter import regex # noqa
97
98
 
98
99
  from dataiter.aggregate import all # noqa
99
100
  from dataiter.aggregate import any # noqa
@@ -20,6 +20,7 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
+ import codecs
23
24
  import dataiter
24
25
  import functools
25
26
  import itertools
@@ -474,16 +475,29 @@ class DataFrame(dict):
474
475
  yield colname, np.delete(column, rows)
475
476
 
476
477
  @classmethod
478
+ @deco.new_from_generator
477
479
  def from_arrow(cls, data, *, dtypes={}):
478
480
  """
479
481
  Return a new data frame from ``pyarrow.Table`` `data`.
480
482
 
481
483
  `dtypes` is an optional dict mapping column names to NumPy datatypes.
482
484
  """
483
- # Arrow's 'to_numpy' is "limited to primitive types for which NumPy has
484
- # the same physical representation as Arrow, and assuming the Arrow
485
- # data has no nulls." Using Pandas is easier and probably good enough.
486
- return cls.from_pandas(data.to_pandas(), dtypes=dtypes)
485
+ for name, column in zip(data.column_names, data.columns):
486
+ req_dtype = dtypes.get(name, None)
487
+ na = column.is_null(nan_is_null=True).to_numpy()
488
+ column = column.to_numpy()
489
+ if np.issubdtype(column.dtype, np.object_):
490
+ if req_dtype is None or np.dtype(req_dtype) != np.dtype(object):
491
+ # Likely to be strings, but not necessarily.
492
+ # Force type-guessing in Vector.fast.
493
+ column = column.tolist()
494
+ column = DataFrameColumn.fast(column, req_dtype)
495
+ if na.any():
496
+ if column.dtype != column.na_dtype:
497
+ # Upcast integer to float if needed.
498
+ column = column.astype(column.na_dtype)
499
+ column[na] = column.na_value
500
+ yield name, column
487
501
 
488
502
  @classmethod
489
503
  def from_json(cls, string, *, columns=[], dtypes={}, **kwargs):
@@ -508,21 +522,29 @@ class DataFrame(dict):
508
522
  return cls(**data)
509
523
 
510
524
  @classmethod
525
+ @deco.new_from_generator
511
526
  def from_pandas(cls, data, *, dtypes={}):
512
527
  """
513
528
  Return a new data frame from ``pandas.DataFrame`` `data`.
514
529
 
515
530
  `dtypes` is an optional dict mapping column names to NumPy datatypes.
516
531
  """
517
- data = {x: data[x].to_numpy(copy=True) for x in data.columns}
518
- for name, value in data.items():
519
- # Pandas object columns are likely to be strings,
520
- # convert to list to force type guessing in Vector.__init__.
521
- if np.issubdtype(value.dtype, np.object_):
522
- data[name] = data[name].tolist()
523
- for name, dtype in dtypes.items():
524
- data[name] = DataFrameColumn(data[name], dtype)
525
- return cls(**data)
532
+ for name in data.columns:
533
+ req_dtype = dtypes.get(name, None)
534
+ na = data[name].isna().to_numpy(copy=True)
535
+ column = data[name].to_numpy(copy=True)
536
+ if np.issubdtype(column.dtype, np.object_):
537
+ if req_dtype is None or np.dtype(req_dtype) != np.dtype(object):
538
+ # Likely to be strings, but not necessarily.
539
+ # Force type-guessing in Vector.fast.
540
+ column = column.tolist()
541
+ column = DataFrameColumn.fast(column, req_dtype)
542
+ if na.any():
543
+ if column.dtype != column.na_dtype:
544
+ # Upcast integer to float if needed.
545
+ column = column.astype(column.na_dtype)
546
+ column[na] = column.na_value
547
+ yield name, column
526
548
 
527
549
  def full_join(self, other, *by):
528
550
  """
@@ -720,8 +742,9 @@ class DataFrame(dict):
720
742
  self._check_dimensions()
721
743
  return len(self)
722
744
 
723
- def _new(self, *args, **kwargs):
724
- return self.__class__(*args, **kwargs)
745
+ @classmethod
746
+ def _new(cls, *args, **kwargs):
747
+ return cls(*args, **kwargs)
725
748
 
726
749
  @property
727
750
  def nrow(self):
@@ -857,19 +880,17 @@ class DataFrame(dict):
857
880
  `columns` is an optional list of columns to limit to. `dtypes` is an
858
881
  optional dict mapping column names to NumPy datatypes.
859
882
  """
860
- import pandas as pd
861
- data = pd.read_csv(path,
862
- sep=sep,
863
- header=0 if header else None,
864
- usecols=columns or None,
865
- dtype=dtypes,
866
- parse_dates=False,
867
- encoding=encoding,
868
- low_memory=False)
883
+ from pyarrow import csv
884
+ table = csv.read_csv(
885
+ path,
886
+ read_options=csv.ReadOptions(encoding=encoding, autogenerate_column_names=not header),
887
+ parse_options=csv.ParseOptions(delimiter=sep, newlines_in_values=True),
888
+ convert_options=csv.ConvertOptions(include_columns=columns))
869
889
 
870
890
  if not header:
871
- data.columns = util.generate_colnames(len(data.columns))
872
- return cls.from_pandas(data, dtypes=dtypes)
891
+ names = util.generate_colnames(table.shape[1])
892
+ table = table.rename_columns(names)
893
+ return cls.from_arrow(table, dtypes=dtypes)
873
894
 
874
895
  @classmethod
875
896
  def read_json(cls, path, *, encoding="utf-8", columns=[], dtypes={}, **kwargs):
@@ -1255,9 +1276,18 @@ class DataFrame(dict):
1255
1276
 
1256
1277
  Will automatically compress if `path` ends in ``.bz2|.gz|.xz``.
1257
1278
  """
1258
- data = self.to_pandas()
1279
+ from pyarrow import csv
1280
+ table = self.to_arrow()
1259
1281
  util.makedirs_for_file(path)
1260
- data.to_csv(path, sep=sep, header=header, index=False, encoding=encoding)
1282
+ csv.write_csv(table, path, write_options=csv.WriteOptions(include_header=header,
1283
+ delimiter=sep,
1284
+ quoting_style="needed"))
1285
+
1286
+ # XXX: Arrow's WriteOptions doesn't include encoding.
1287
+ # If user requested something else than UTF-8, we need to rewrite!
1288
+ if codecs.lookup(encoding) != codecs.lookup("utf-8"):
1289
+ with util.xopen(path, "rt", encoding="utf-8") as f: text = f.read()
1290
+ with util.xopen(path, "wt", encoding=encoding) as f: f.write(text)
1261
1291
 
1262
1292
  def write_json(self, path, *, encoding="utf-8", **kwargs):
1263
1293
  """
@@ -50,7 +50,6 @@ def from_string(x, format):
50
50
  if util.is_scalar(x):
51
51
  x = Vector([x], str)
52
52
  return from_string(x, format)[0]
53
- x = util.sequencify(x)
54
53
  assert isinstance(x, np.ndarray)
55
54
  assert isinstance(x.dtype, StringDType)
56
55
  out = np.full_like(x, None, object)
@@ -150,7 +149,6 @@ def _pull_datetime(x, function):
150
149
  if util.is_scalar(x):
151
150
  x = Vector([x], np.datetime64)
152
151
  return _pull_datetime(x, function)[0]
153
- x = util.sequencify(x)
154
152
  assert isinstance(x, np.ndarray)
155
153
  assert np.issubdtype(x.dtype, np.datetime64)
156
154
  out = np.full_like(x, np.nan)
@@ -165,7 +163,6 @@ def _pull_int(x, function):
165
163
  if util.is_scalar(x):
166
164
  x = Vector([x], np.datetime64)
167
165
  return _pull_int(x, function)[0]
168
- x = util.sequencify(x)
169
166
  assert isinstance(x, np.ndarray)
170
167
  assert np.issubdtype(x.dtype, np.datetime64)
171
168
  out = np.full_like(x, np.nan, float)
@@ -180,7 +177,6 @@ def _pull_str(x, function):
180
177
  if util.is_scalar(x):
181
178
  x = Vector([x], np.datetime64)
182
179
  return _pull_str(x, function)[0]
183
- x = util.sequencify(x)
184
180
  assert isinstance(x, np.ndarray)
185
181
  assert np.issubdtype(x.dtype, np.datetime64)
186
182
  out = np.full_like(x, dtypes.string.na_object, object)
@@ -216,7 +212,6 @@ def replace(x, year=None, month=None, day=None, hour=None, minute=None, second=N
216
212
  scalar_keys = [x for x in kwargs if util.is_scalar(kwargs[x])]
217
213
  vector_keys = [x for x in kwargs if x not in scalar_keys]
218
214
  # Like _pull_datetime, but no vectorized function.
219
- x = util.sequencify(x)
220
215
  assert isinstance(x, np.ndarray)
221
216
  assert np.issubdtype(x.dtype, np.datetime64)
222
217
  out = np.full_like(x, np.nan)
@@ -20,6 +20,8 @@
20
20
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
21
  # THE SOFTWARE.
22
22
 
23
+ import warnings
24
+
23
25
  from numpy.dtypes import StringDType
24
26
 
25
27
  #: Instance of NumPy variable-width StringDType used
@@ -30,3 +32,7 @@ string = StringDType(na_object="")
30
32
  # input, a distinction between NA and blank cannot usually be made.
31
33
  # TODO: Consider changing this to something like ':NA:'.
32
34
  # https://numpy.org/doc/stable/user/basics.strings.html#missing-data-support
35
+
36
+ # Ignore pointless warnings about using StringDType in numpy.savez.
37
+ _pattern = "Custom dtypes are saved as python objects using the pickle protocol"
38
+ warnings.filterwarnings("ignore", message=_pattern, category=UserWarning)
@@ -0,0 +1,148 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Copyright (c) 2025 Osmo Salomaa
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ import numpy as np
24
+ import re
25
+
26
+ from dataiter import dtypes
27
+ from dataiter import util
28
+ from dataiter import Vector
29
+ from numpy.dtypes import StringDType
30
+
31
+ def _prep(string, dtype, default):
32
+ assert isinstance(string, np.ndarray)
33
+ assert isinstance(string.dtype, StringDType)
34
+ out = np.full_like(string, default, dtype)
35
+ na = string == dtypes.string.na_object
36
+ return out, na
37
+
38
+ def findall(pattern, string, flags=0):
39
+ """
40
+ Return a list of matches of `pattern` in `string`.
41
+
42
+ https://docs.python.org/3/library/re.html#re.findall
43
+
44
+ >>> x = di.Vector(["asdf", "1234"])
45
+ >>> regex.findall(r"[a-z]", x)
46
+ """
47
+ if util.is_scalar(string):
48
+ return re.findall(pattern, string, flags=flags)
49
+ out, na = _prep(string, object, None)
50
+ for i in np.flatnonzero(~na):
51
+ out[i] = re.findall(pattern, string[i], flags=flags)
52
+ return Vector.fast(out, object)
53
+
54
+ def fullmatch(pattern, string, flags=0):
55
+ """
56
+ Return a ``re.Match`` object or ``None``.
57
+
58
+ https://docs.python.org/3/library/re.html#re.fullmatch
59
+
60
+ >>> x = di.Vector(["asdf", "1234"])
61
+ >>> regex.fullmatch(r"[a-z]+", x)
62
+ """
63
+ if util.is_scalar(string):
64
+ return re.fullmatch(pattern, string, flags=flags)
65
+ out, na = _prep(string, object, None)
66
+ for i in np.flatnonzero(~na):
67
+ out[i] = re.fullmatch(pattern, string[i], flags=flags)
68
+ return Vector.fast(out, object)
69
+
70
+ def match(pattern, string, flags=0):
71
+ """
72
+ Return a ``re.Match`` object or ``None``.
73
+
74
+ https://docs.python.org/3/library/re.html#re.match
75
+
76
+ >>> x = di.Vector(["asdf", "1234"])
77
+ >>> regex.match(r"[a-z]", x)
78
+ """
79
+ if util.is_scalar(string):
80
+ return re.match(pattern, string, flags=flags)
81
+ out, na = _prep(string, object, None)
82
+ for i in np.flatnonzero(~na):
83
+ out[i] = re.match(pattern, string[i], flags=flags)
84
+ return Vector.fast(out, object)
85
+
86
+ def search(pattern, string, flags=0):
87
+ """
88
+ Return a ``re.Match`` object or ``None``.
89
+
90
+ https://docs.python.org/3/library/re.html#re.search
91
+
92
+ >>> x = di.Vector(["asdf", "1234"])
93
+ >>> regex.search(r"[a-z]", x)
94
+ """
95
+ if util.is_scalar(string):
96
+ return re.search(pattern, string, flags=flags)
97
+ out, na = _prep(string, object, None)
98
+ for i in np.flatnonzero(~na):
99
+ out[i] = re.search(pattern, string[i], flags=flags)
100
+ return Vector.fast(out, object)
101
+
102
+ def split(pattern, string, maxsplit=0, flags=0):
103
+ """
104
+ Return a list of `string` split by `pattern`.
105
+
106
+ https://docs.python.org/3/library/re.html#re.split
107
+
108
+ >>> x = di.Vector(["one two three", "four"])
109
+ >>> regex.split(r" +", x)
110
+ """
111
+ if util.is_scalar(string):
112
+ return re.split(pattern, string, maxsplit=maxsplit, flags=flags)
113
+ out, na = _prep(string, object, None)
114
+ for i in np.flatnonzero(~na):
115
+ out[i] = re.split(pattern, string[i], maxsplit=maxsplit, flags=flags)
116
+ return Vector.fast(out, object)
117
+
118
+ def sub(pattern, repl, string, count=0, flags=0):
119
+ """
120
+ Return `string` with instances of `pattern` replaced with `repl`.
121
+
122
+ https://docs.python.org/3/library/re.html#re.sub
123
+
124
+ >>> x = di.Vector(["great", "fantastic"])
125
+ >>> regex.sub(r"$", r"!", x)
126
+ """
127
+ if util.is_scalar(string):
128
+ return re.sub(pattern, repl, string, count=count, flags=flags)
129
+ out, na = _prep(string, dtypes.string, dtypes.string.na_object)
130
+ for i in np.flatnonzero(~na):
131
+ out[i] = re.sub(pattern, repl, string[i], count=count, flags=flags)
132
+ return Vector.fast(out, str)
133
+
134
+ def subn(pattern, repl, string, count=0, flags=0):
135
+ """
136
+ Return `string`, count of instances of `pattern` replaced with `repl`.
137
+
138
+ https://docs.python.org/3/library/re.html#re.subn
139
+
140
+ >>> x = di.Vector(["great", "fantastic"])
141
+ >>> regex.subn(r"$", r"!", x)
142
+ """
143
+ if util.is_scalar(string):
144
+ return re.subn(pattern, repl, string, count=count, flags=flags)
145
+ out, na = _prep(string, object, None)
146
+ for i in np.flatnonzero(~na):
147
+ out[i] = re.subn(pattern, repl, string[i], count=count, flags=flags)
148
+ return Vector.fast(out, object)
@@ -31,9 +31,8 @@ from dataiter import test
31
31
  from pathlib import Path
32
32
  from unittest.mock import patch
33
33
 
34
- # This UserWarning seems unavoidable when using StringDtype, which unlike
35
- # other dtypes must be instantiated and that custom instance used.
36
- IGNORE_NPZ_PICKLE_WARNING = "ignore:Custom dtypes are saved as python objects using the pickle protocol."
34
+ # See dataiter/dtypes.py for the equivalent non-test ignore.
35
+ IGNORE_NPZ_PICKLE_WARNING = "ignore:Custom dtypes are saved as python objects using the pickle protocol"
37
36
 
38
37
  class TestDataFrameColumn:
39
38
 
@@ -448,6 +447,70 @@ class TestDataFrame:
448
447
  assert data.make.is_object()
449
448
  assert data.model.is_object()
450
449
 
450
+ def test_read_csv_missing_blank(self):
451
+ text = """
452
+ int,float,string,bool,date
453
+ ,2.5,"test",TRUE,2025-01-01
454
+ 1,,"test",TRUE,2025-01-01
455
+ 1,2.5,"",TRUE,2025-01-01
456
+ 1,2.5,"test",,2025-01-01
457
+ 1,2.5,"test",TRUE,
458
+ """.strip()
459
+ handle, path = tempfile.mkstemp(".csv")
460
+ Path(path).write_text(text, "utf-8")
461
+ data = DataFrame.read_csv(path)
462
+ for i, column in enumerate(data.columns):
463
+ assert column.is_na().sum() == 1
464
+ assert column.is_na()[i]
465
+
466
+ def test_read_csv_missing_excel(self):
467
+ text = """
468
+ int,float,string,bool,date
469
+ #N/A,2.5,"test",TRUE,2025-01-01
470
+ 1,#N/A,"test",TRUE,2025-01-01
471
+ 1,2.5,"",TRUE,2025-01-01
472
+ 1,2.5,"test",#N/A,2025-01-01
473
+ 1,2.5,"test",TRUE,#N/A
474
+ """.strip()
475
+ handle, path = tempfile.mkstemp(".csv")
476
+ Path(path).write_text(text, "utf-8")
477
+ data = DataFrame.read_csv(path)
478
+ for i, column in enumerate(data.columns):
479
+ assert column.is_na().sum() == 1
480
+ assert column.is_na()[i]
481
+
482
+ def test_read_csv_missing_na(self):
483
+ text = """
484
+ int,float,string,bool,date
485
+ NA,2.5,"test",TRUE,2025-01-01
486
+ 1,NA,"test",TRUE,2025-01-01
487
+ 1,2.5,"",TRUE,2025-01-01
488
+ 1,2.5,"test",NA,2025-01-01
489
+ 1,2.5,"test",TRUE,NA
490
+ """.strip()
491
+ handle, path = tempfile.mkstemp(".csv")
492
+ Path(path).write_text(text, "utf-8")
493
+ data = DataFrame.read_csv(path)
494
+ for i, column in enumerate(data.columns):
495
+ assert column.is_na().sum() == 1
496
+ assert column.is_na()[i]
497
+
498
+ def test_read_csv_missing_null(self):
499
+ text = """
500
+ int,float,string,bool,date
501
+ NULL,2.5,"test",TRUE,2025-01-01
502
+ 1,NULL,"test",TRUE,2025-01-01
503
+ 1,2.5,"",TRUE,2025-01-01
504
+ 1,2.5,"test",NULL,2025-01-01
505
+ 1,2.5,"test",TRUE,NULL
506
+ """.strip()
507
+ handle, path = tempfile.mkstemp(".csv")
508
+ Path(path).write_text(text, "utf-8")
509
+ data = DataFrame.read_csv(path)
510
+ for i, column in enumerate(data.columns):
511
+ assert column.is_na().sum() == 1
512
+ assert column.is_na()[i]
513
+
451
514
  def test_read_json(self):
452
515
  path = str(test.get_data_path("downloads.json"))
453
516
  data = DataFrame.read_json(path)
@@ -0,0 +1,92 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # Copyright (c) 2025 Osmo Salomaa
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
22
+
23
+ import re
24
+
25
+ from dataiter import regex
26
+ from dataiter import Vector
27
+
28
+ class TestRegex:
29
+
30
+ def test_findall(self):
31
+ pattern = r"[a-z]"
32
+ string = Vector(["asdf", "1234", ""])
33
+ result = regex.findall(pattern, string)
34
+ expected = [["a", "s", "d", "f"], [], None]
35
+ assert result.tolist() == expected
36
+ assert regex.findall(pattern, string[0]) == result[0]
37
+
38
+ def test_fullmatch(self):
39
+ pattern = r"[a-z]+"
40
+ string = Vector(["asdf", "1234", ""])
41
+ result = regex.fullmatch(pattern, string)
42
+ assert isinstance(result[0], re.Match)
43
+ assert result[1] is None
44
+ assert result[2] is None
45
+ match = regex.fullmatch(pattern, string[0])
46
+ assert isinstance(match, re.Match)
47
+
48
+ def test_match(self):
49
+ pattern = r"[a-z]"
50
+ string = Vector(["asdf", "1234", ""])
51
+ result = regex.match(pattern, string)
52
+ assert isinstance(result[0], re.Match)
53
+ assert result[1] is None
54
+ assert result[2] is None
55
+ match = regex.match(pattern, string[0])
56
+ assert isinstance(match, re.Match)
57
+
58
+ def test_search(self):
59
+ pattern = r"[a-z]"
60
+ string = Vector(["asdf", "1234", ""])
61
+ result = regex.search(pattern, string)
62
+ assert isinstance(result[0], re.Match)
63
+ assert result[1] is None
64
+ assert result[2] is None
65
+ match = regex.search(pattern, string[0])
66
+ assert isinstance(match, re.Match)
67
+
68
+ def test_split(self):
69
+ pattern = r" +"
70
+ string = Vector(["one two three", "four", ""])
71
+ result = regex.split(pattern, string)
72
+ expected = [["one", "two", "three"], ["four"], None]
73
+ assert result.tolist() == expected
74
+ assert regex.split(pattern, string[0]) == result[0]
75
+
76
+ def test_sub(self):
77
+ pattern = r"$"
78
+ repl = "!"
79
+ string = Vector(["great", "fantastic", ""])
80
+ result = regex.sub(pattern, repl, string)
81
+ expected = ["great!", "fantastic!", None]
82
+ assert result.tolist() == expected
83
+ assert regex.sub(pattern, repl, string[0]) == result[0]
84
+
85
+ def test_subn(self):
86
+ pattern = r"$"
87
+ repl = "!"
88
+ string = Vector(["great", "fantastic", ""])
89
+ result = regex.subn(pattern, repl, string)
90
+ expected = [("great!", 1), ("fantastic!", 1), None]
91
+ assert result.tolist() == expected
92
+ assert regex.subn(pattern, repl, string[0]) == result[0]
@@ -234,6 +234,12 @@ class TestVector:
234
234
  b = a.drop_na()
235
235
  assert b.tolist() == [1, 2, 3]
236
236
 
237
+ def test_dt(self):
238
+ a = Vector(["2025-01-11"], np.datetime64)
239
+ assert a.dt.year().tolist() == [2025]
240
+ assert a.dt.month().tolist() == [1]
241
+ assert a.dt.day().tolist() == [11]
242
+
237
243
  def test_equal_boolean(self):
238
244
  a = Vector([True, False])
239
245
  b = Vector([True, False])
@@ -463,6 +469,10 @@ class TestVector:
463
469
  assert a.rank(method="min").equal(a.rank(method="max"))
464
470
  assert a.rank(method="min").equal(a.rank(method="ordinal"))
465
471
 
472
+ def test_re(self):
473
+ a = Vector(["great", "fantastic"])
474
+ assert a.re.sub(r"$", r"!").tolist() == ["great!", "fantastic!"]
475
+
466
476
  def test_replace_na(self):
467
477
  a = Vector([1, 2, 3, None])
468
478
  assert a.replace_na(0).tolist() == [1, 2, 3, 0]
@@ -492,6 +502,12 @@ class TestVector:
492
502
  assert a.sort(dir=1).tolist() == [1, 2, 3, 4, 5, None, None]
493
503
  assert a.sort(dir=-1).tolist() == [5, 4, 3, 2, 1, None, None]
494
504
 
505
+ def test_str(self):
506
+ a = Vector(["asdf", "1234"])
507
+ assert a.str.isalpha().tolist() == [True, False]
508
+ assert a.str.str_len().tolist() == [4, 4]
509
+ assert a.str.upper().tolist() == ["ASDF", "1234"]
510
+
495
511
  def test_tail(self):
496
512
  a = Vector([1, 2, 3, 4, 5])
497
513
  assert a.tail(3).tolist() == [3, 4, 5]
@@ -22,6 +22,7 @@
22
22
 
23
23
  import dataiter
24
24
  import datetime
25
+ import functools
25
26
  import numpy as np
26
27
  import sys
27
28
 
@@ -35,6 +36,103 @@ TYPE_CONVERSIONS = {
35
36
  datetime.datetime: "datetime64[us]",
36
37
  }
37
38
 
39
+ def as_vector(function):
40
+ @functools.wraps(function)
41
+ def wrapper(*args, **kwargs):
42
+ array = function(*args, **kwargs)
43
+ return array.view(Vector)
44
+ return wrapper
45
+
46
+ def not_implemented(*args, **kwargs):
47
+ raise NotImplementedError
48
+
49
+ class DtProxy:
50
+ def __init__(self, vector):
51
+ from dataiter import dt
52
+ wrap = lambda f: functools.partial(f, vector)
53
+ self.day = wrap(dt.day)
54
+ self.from_string = wrap(dt.from_string)
55
+ self.hour = wrap(dt.hour)
56
+ self.isoweek = wrap(dt.isoweek)
57
+ self.isoweekday = wrap(dt.isoweekday)
58
+ self.microsecond = wrap(dt.microsecond)
59
+ self.minute = wrap(dt.minute)
60
+ self.month = wrap(dt.month)
61
+ self.new = wrap(dt.new)
62
+ self.quarter = wrap(dt.quarter)
63
+ self.replace = wrap(dt.replace)
64
+ self.second = wrap(dt.second)
65
+ self.to_string = wrap(dt.to_string)
66
+ self.weekday = wrap(dt.weekday)
67
+ self.year = wrap(dt.year)
68
+
69
+ class ReProxy:
70
+ def __init__(self, vector):
71
+ from dataiter import regex
72
+ wrap = lambda f: functools.partial(f, string=vector)
73
+ self.findall = wrap(regex.findall)
74
+ self.fullmatch = wrap(regex.fullmatch)
75
+ self.match = wrap(regex.match)
76
+ self.search = wrap(regex.search)
77
+ self.split = wrap(regex.split)
78
+ self.sub = wrap(regex.sub)
79
+ self.subn = wrap(regex.subn)
80
+
81
+ class StrProxy:
82
+ def __init__(self, vector):
83
+ wrap = lambda name: as_vector(functools.partial(
84
+ getattr(np.strings, name, not_implemented),
85
+ vector))
86
+
87
+ # https://numpy.org/doc/stable/reference/routines.strings.html
88
+ self.add = wrap("add")
89
+ self.capitalize = wrap("capitalize")
90
+ self.center = wrap("center")
91
+ self.count = wrap("count")
92
+ self.decode = wrap("decode")
93
+ self.encode = wrap("encode")
94
+ self.endswith = wrap("endswith")
95
+ self.equal = wrap("equal")
96
+ self.expandtabs = wrap("expandtabs")
97
+ self.find = wrap("find")
98
+ self.greater = wrap("greater")
99
+ self.greater_equal = wrap("greater_equal")
100
+ self.index = wrap("index")
101
+ self.isalnum = wrap("isalnum")
102
+ self.isalpha = wrap("isalpha")
103
+ self.isdecimal = wrap("isdecimal")
104
+ self.isdigit = wrap("isdigit")
105
+ self.islower = wrap("islower")
106
+ self.isnumeric = wrap("isnumeric")
107
+ self.isspace = wrap("isspace")
108
+ self.istitle = wrap("istitle")
109
+ self.isupper = wrap("isupper")
110
+ self.less = wrap("less")
111
+ self.less_equal = wrap("less_equal")
112
+ self.ljust = wrap("ljust")
113
+ self.lower = wrap("lower")
114
+ self.lstrip = wrap("lstrip")
115
+ self.mod = wrap("mod")
116
+ self.multiply = wrap("multiply")
117
+ self.not_equal = wrap("not_equal")
118
+ self.replace = wrap("replace")
119
+ self.rfind = wrap("rfind")
120
+ self.rindex = wrap("rindex")
121
+ self.rjust = wrap("rjust")
122
+ self.rstrip = wrap("rstrip")
123
+ self.startswith = wrap("startswith")
124
+ self.str_len = wrap("str_len")
125
+ self.strip = wrap("strip")
126
+ self.swapcase = wrap("swapcase")
127
+ self.title = wrap("title")
128
+ self.translate = wrap("translate")
129
+ self.upper = wrap("upper")
130
+ self.zfill = wrap("zfill")
131
+
132
+ # Skip functions that have a difficult return value.
133
+ # self.partition = wrap("partition")
134
+ # self.rpartition = wrap("rpartition")
135
+
38
136
  class Vector(np.ndarray):
39
137
 
40
138
  """
@@ -76,6 +174,8 @@ class Vector(np.ndarray):
76
174
 
77
175
  >>> di.Vector([1, 2, 3], int)
78
176
  """
177
+ # Note that when we call array.view(Vector) for a numpy.ndarray,
178
+ # this will not be called and no attributes added here will exist!
79
179
  self._check_dimensions()
80
180
 
81
181
  def __array_wrap__(self, array, context=None, return_scalar=False):
@@ -108,8 +208,7 @@ class Vector(np.ndarray):
108
208
  >>> vector.as_bytes()
109
209
  """
110
210
  if self.is_string():
111
- array = np.strings.encode(self, "utf-8")
112
- return array.view(self.__class__)
211
+ return self.str.encode("utf-8")
113
212
  return self.astype(bytes)
114
213
 
115
214
  def as_date(self):
@@ -192,6 +291,20 @@ class Vector(np.ndarray):
192
291
  """
193
292
  return self[~self.is_na()].copy()
194
293
 
294
+ @property
295
+ def dt(self) -> DtProxy:
296
+ """
297
+ Proxy object for calling :mod:`dataiter.dt` functions.
298
+
299
+ >>> x = di.Vector(["2025-01-11"], np.datetime64)
300
+ >>> x.dt.year()
301
+ >>> x.dt.month()
302
+ >>> x.dt.day()
303
+ """
304
+ if not hasattr(self, "_dt"):
305
+ self._dt = DtProxy(self)
306
+ return self._dt
307
+
195
308
  @property
196
309
  def dtype_label(self):
197
310
  """
@@ -457,7 +570,7 @@ class Vector(np.ndarray):
457
570
  return array
458
571
 
459
572
  def _optimize_for_argsort(self):
460
- if self.is_string() and (n := np.strings.str_len(self).max()) < 50:
573
+ if self.is_string() and (n := self.str.str_len().max()) < 50:
461
574
  # XXX: We get a huge speed boost often by converting
462
575
  # to the old-style fixed-width strings! This is probably
463
576
  # temporary and can be removed once StringDType has matured.
@@ -525,6 +638,18 @@ class Vector(np.ndarray):
525
638
  return out.view(self.__class__)
526
639
  raise ValueError(f"Unexpected method: {method!r}")
527
640
 
641
+ @property
642
+ def re(self) -> ReProxy:
643
+ """
644
+ Proxy object for calling :mod:`dataiter.regex` functions.
645
+
646
+ >>> x = di.Vector(["great", "fantastic"])
647
+ >>> x.re.sub(r"$", r"!")
648
+ """
649
+ if not hasattr(self, "_re"):
650
+ self._re = ReProxy(self)
651
+ return self._re
652
+
528
653
  def replace_na(self, value):
529
654
  """
530
655
  Return vector with missing values replaced with `value`.
@@ -623,6 +748,22 @@ class Vector(np.ndarray):
623
748
  # Usually causes dtype to be object!
624
749
  return None
625
750
 
751
+ @property
752
+ def str(self) -> StrProxy:
753
+ """
754
+ Proxy object for calling ``numpy.strings`` functions.
755
+
756
+ https://numpy.org/doc/stable/reference/routines.strings.html
757
+
758
+ >>> x = di.Vector(["asdf", "1234"])
759
+ >>> x.str.isalpha()
760
+ >>> x.str.str_len()
761
+ >>> x.str.upper()
762
+ """
763
+ if not hasattr(self, "_str"):
764
+ self._str = StrProxy(self)
765
+ return self._str
766
+
626
767
  def tail(self, n=None):
627
768
  """
628
769
  Return the last `n` elements.
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["hatchling==1.21.1"]
2
+ requires = ["build==1.2.2.post1", "hatchling==1.21.1"]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -10,7 +10,7 @@ readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.9.0"
12
12
  authors = [{ name = "Osmo Salomaa", email = "otsaloma@iki.fi" }]
13
- dependencies = ["attd>=0.3", "numpy>=2.0,<3.0", "pandas>=1.0", "wcwidth>=0.1"]
13
+ dependencies = ["attd>=0.3", "numpy>=2.0,<3.0", "pyarrow>=2.0", "wcwidth>=0.1"]
14
14
 
15
15
  [project.urls]
16
16
  Homepage = "https://github.com/otsaloma/dataiter"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes