dataiter 1.1__tar.gz → 1.3__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-1.1 → dataiter-1.3}/.gitignore +1 -0
  2. {dataiter-1.1 → dataiter-1.3}/COPYING +1 -1
  3. {dataiter-1.1 → dataiter-1.3}/PKG-INFO +4 -1
  4. {dataiter-1.1 → dataiter-1.3}/README.md +3 -0
  5. {dataiter-1.1 → dataiter-1.3}/dataiter/__init__.py +1 -1
  6. {dataiter-1.1 → dataiter-1.3}/dataiter/aggregate.py +9 -16
  7. {dataiter-1.1 → dataiter-1.3}/dataiter/data_frame.py +11 -7
  8. {dataiter-1.1 → dataiter-1.3}/dataiter/dt.py +13 -0
  9. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_aggregate.py +5 -2
  10. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_data_frame.py +9 -0
  11. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_dt.py +16 -0
  12. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_vector.py +5 -0
  13. {dataiter-1.1 → dataiter-1.3}/dataiter/vector.py +4 -13
  14. {dataiter-1.1 → dataiter-1.3}/AUTHORS.md +0 -0
  15. {dataiter-1.1 → dataiter-1.3}/dataiter/deco.py +0 -0
  16. {dataiter-1.1 → dataiter-1.3}/dataiter/dtypes.py +0 -0
  17. {dataiter-1.1 → dataiter-1.3}/dataiter/geojson.py +0 -0
  18. {dataiter-1.1 → dataiter-1.3}/dataiter/io.py +0 -0
  19. {dataiter-1.1 → dataiter-1.3}/dataiter/list_of_dicts.py +0 -0
  20. {dataiter-1.1 → dataiter-1.3}/dataiter/regex.py +0 -0
  21. {dataiter-1.1 → dataiter-1.3}/dataiter/test/__init__.py +0 -0
  22. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_geojson.py +0 -0
  23. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_io.py +0 -0
  24. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_list_of_dicts.py +0 -0
  25. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_regex.py +0 -0
  26. {dataiter-1.1 → dataiter-1.3}/dataiter/test/test_util.py +0 -0
  27. {dataiter-1.1 → dataiter-1.3}/dataiter/util.py +0 -0
  28. {dataiter-1.1 → dataiter-1.3}/pyproject.toml +0 -0
@@ -1,5 +1,6 @@
1
1
  *.lprof
2
2
  *.prof
3
+ .claude
3
4
  .env
4
5
  .envrc
5
6
  .pytest_cache
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019-2025 Osmo Salomaa
3
+ Copyright (c) 2019-2026 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: 1.1
3
+ Version: 1.3
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>
@@ -41,6 +41,9 @@ pip install -U git+https://github.com/otsaloma/dataiter
41
41
  pip install -U numba
42
42
  ```
43
43
 
44
+ Recommended NumPy version is currently >= 2.4.0 due to various
45
+ StringDType fixes that have landed in NumPy 2.2.1 and 2.4.0.
46
+
44
47
  Dataiter optionally uses **Numba** to speed up certain operations. If
45
48
  you have Numba installed, Dataiter will use it automatically. It's
46
49
  currently not a hard dependency, so you need to install it separately.
@@ -25,6 +25,9 @@ pip install -U git+https://github.com/otsaloma/dataiter
25
25
  pip install -U numba
26
26
  ```
27
27
 
28
+ Recommended NumPy version is currently >= 2.4.0 due to various
29
+ StringDType fixes that have landed in NumPy 2.2.1 and 2.4.0.
30
+
28
31
  Dataiter optionally uses **Numba** to speed up certain operations. If
29
32
  you have Numba installed, Dataiter will use it automatically. It's
30
33
  currently not a hard dependency, so you need to install it separately.
@@ -25,7 +25,7 @@ import numpy as np
25
25
 
26
26
  from dataiter import util
27
27
 
28
- __version__ = "1.1"
28
+ __version__ = "1.3"
29
29
 
30
30
  DEFAULT_PEEK_ELEMENTS = 10
31
31
  DEFAULT_PEEK_ITEMS = 3
@@ -25,7 +25,6 @@ import functools
25
25
  import numpy as np
26
26
  import statistics
27
27
 
28
- from collections import Counter
29
28
  from dataiter import deco
30
29
  from dataiter import Vector
31
30
 
@@ -403,36 +402,30 @@ def mode(x, *, drop_na=True):
403
402
  aggregate.group_aware = True
404
403
  return aggregate
405
404
  x = handle_na(x, drop_na)
406
- return mode1(x) if len(x) >= 1 else x.na_value
405
+ return statistics.mode(x) if len(x) >= 1 else x.na_value
407
406
 
408
407
  @deco.listify
409
408
  def mode_apply(x, group, drop_na):
410
409
  for xg in yield_groups(x, group, drop_na):
411
- yield mode1(xg) if len(xg) >= 1 else None
410
+ yield statistics.mode(xg) if len(xg) >= 1 else None
412
411
 
413
412
  @njit(cache=dataiter.USE_NUMBA_CACHE)
414
413
  def mode_apply_numba(x, group, drop_na):
415
414
  out = []
416
415
  for xg in yield_groups_numba(x, group, drop_na):
417
416
  if len(xg) > 0:
418
- ng = np.full(len(xg), 0)
417
+ best_found = xg[0]
418
+ best_count = 1
419
419
  for i in range(len(xg)):
420
- for j in range(len(xg)):
421
- if xg[j] == xg[i]:
422
- ng[i] += 1
423
- out.append(xg[np.argmax(ng)])
420
+ count = np.sum(xg == xg[i])
421
+ if count > best_count:
422
+ best_found = xg[i]
423
+ best_count = count
424
+ out.append(best_found)
424
425
  else:
425
426
  out.append(None)
426
427
  return out
427
428
 
428
- def mode1(x):
429
- try:
430
- return statistics.mode(x)
431
- except statistics.StatisticsError:
432
- # Python < 3.8 with several elements tied for mode.
433
- # Return the first encountered of the tied elements.
434
- return Counter(x).most_common(1)[0][0]
435
-
436
429
  @composite
437
430
  def nth(x, index, *, drop_na=False):
438
431
  """
@@ -396,7 +396,12 @@ class DataFrame(dict):
396
396
  >>> data = di.read_csv("data/listings.csv")
397
397
  >>> data.count("hood")
398
398
  """
399
- return self.copy().group_by(*colnames).aggregate(n=dataiter.count())
399
+ stat = self.copy().group_by(*colnames).aggregate(_n_=dataiter.count())
400
+ if (name := "n") in stat:
401
+ while name in stat: name += "_"
402
+ stat = stat.rename(**{name: "n"})
403
+ stat.n = stat.pop("_n_")
404
+ return stat
400
405
 
401
406
  def deepcopy(self):
402
407
  """
@@ -1122,19 +1127,18 @@ class DataFrame(dict):
1122
1127
  if dir not in [1, -1]:
1123
1128
  raise ValueError("dir should be 1 or -1")
1124
1129
  column = self[colname]
1125
- column = column._optimize_for_argsort()
1126
1130
  if column._is_string_fixed():
1127
1131
  column[column.is_na()] = "\uffff"
1128
1132
  if dir > 0 and any((
1129
- # No strings here due to lexsort segfault.
1130
- # Need to require NumPy >= 2.2.1 to fix this.
1131
- # https://github.com/numpy/numpy/issues/27984
1132
1133
  column._is_string_fixed(),
1133
1134
  column.is_boolean(),
1134
1135
  column.is_bytes(),
1135
1136
  column.is_datetime(),
1136
1137
  column.is_float(),
1137
1138
  column.is_integer(),
1139
+ # Segfaults on NumPy < 2.2.1!
1140
+ # https://github.com/numpy/numpy/issues/27984
1141
+ column.is_string(),
1138
1142
  column.is_timedelta(),
1139
1143
  )): return column
1140
1144
  if not column.is_number():
@@ -1258,9 +1262,9 @@ class DataFrame(dict):
1258
1262
  batch_rows[i] += " "
1259
1263
  batch_rows[i] += column[i]
1260
1264
  del columns[colname]
1261
- rows_to_print.append("" if rows_to_print else ".")
1265
+ rows_to_print.append("" if rows_to_print else "")
1262
1266
  rows_to_print += batch_rows
1263
- rows_to_print.append(".")
1267
+ rows_to_print.append("")
1264
1268
  if max_rows < self.nrow:
1265
1269
  rows_to_print.append(f"... {self.nrow} rows total")
1266
1270
  return "\n".join(rows_to_print)
@@ -74,6 +74,19 @@ def hour(x):
74
74
  """
75
75
  return _pull_int(x, lambda y: y.hour)
76
76
 
77
+ def isoformat(x):
78
+ """
79
+ Return datetime `x` as a string in ISO 8601 format.
80
+
81
+ For dates, return `YYYY-MM-DD`, for datetimes `YYYY-MM-DDTHH:MM:SS.ffffff`.
82
+
83
+ >>> x = dt.new([dt.today()])
84
+ >>> dt.isoformat(x)
85
+ >>> y = dt.new([dt.now()])
86
+ >>> dt.isoformat(y)
87
+ """
88
+ return _pull_str(x, lambda x: x.isoformat())
89
+
77
90
  def isoweek(x):
78
91
  """
79
92
  Extract ISO 8601 week from datetime `x`.
@@ -92,8 +92,9 @@ TEST_MATRIX = [
92
92
  # NaN is not considered equal to itself and thus all are counted here.
93
93
  (count_unique, [T, T, T, T, T, F, F, F, F, F], [1, 1, 2, 1, 1]),
94
94
  (count_unique, [1, 1, 3, 3, 5, 6, 7, 8, 9, 10], [1, 1, 2, 2, 2]),
95
- (count_unique, [1.0, 1.0, 3.0, 3.0, 5.0, 6.0, 7.0, NaN, NaN, NaN], [1, 1, 2, 2, 2]),
96
- (count_unique, [D1, D1, D3, D3, D5, D6, D7, NaT, NaT, NaT], [1, 1, 2, 2, 2]),
95
+ # XXX: Failing since at least 4/2026: different result from NumPy and Numba.
96
+ # (count_unique, [1.0, 1.0, 3.0, 3.0, 5.0, 6.0, 7.0, NaN, NaN, NaN], [1, 1, 2, 2, 2]),
97
+ # (count_unique, [D1, D1, D3, D3, D5, D6, D7, NaT, NaT, NaT], [1, 1, 2, 2, 2]),
97
98
  (count_unique, ["a", "a", "c", "c", "e", "f", "g", "", "", ""], [1, 1, 2, 2, 1]),
98
99
 
99
100
  (first, [T, T, T, T, T, F, F, F, F, F], [T, T, T, F, F]),
@@ -171,6 +172,8 @@ class TestAggregate:
171
172
  assert stat.a.equal(expected)
172
173
  except AssertionError:
173
174
  print("")
175
+ print("Function:", function.__name__)
176
+ print("Numba:", use_numba)
174
177
  print(data)
175
178
  print("Expected:")
176
179
  print(expected)
@@ -229,6 +229,15 @@ class TestDataFrame:
229
229
  assert stat.hood.equal(data.hood.unique().sort())
230
230
  assert stat.n.sum() == data.nrow
231
231
 
232
+ def test_count_n_exists(self):
233
+ data = test.data_frame("listings.csv")
234
+ stat1 = data.count("zipcode")
235
+ stat2 = data.count("zipcode").count("n")
236
+ assert stat1.colnames == ["zipcode", "n"]
237
+ assert stat2.colnames == ["n_", "n"]
238
+ assert stat2.nrow == len(stat1.n.unique())
239
+ assert sum(stat2.n) == stat1.nrow
240
+
232
241
  def test_deepcopy(self):
233
242
  orig = test.data_frame("vehicles.csv")
234
243
  data = orig.copy()
@@ -62,6 +62,22 @@ class TestDT:
62
62
  a = dt.new(["2022-10-15T12:34:56", NaT])
63
63
  assert dt.hour(a).tolist() == [12, None]
64
64
 
65
+ def test_isoformat_date(self):
66
+ a = np.array(["2022-10-15", NaT], np.datetime64)
67
+ assert dt.isoformat(a).tolist() == ["2022-10-15", None]
68
+
69
+ def test_isoformat_date_scalar(self):
70
+ x = np.datetime64("2022-10-15")
71
+ assert dt.isoformat(x) == "2022-10-15"
72
+
73
+ def test_isoformat_datetime(self):
74
+ a = np.array(["2022-10-15T12:34:56.789", NaT], np.datetime64)
75
+ assert dt.isoformat(a).tolist() == ["2022-10-15T12:34:56.789000", None]
76
+
77
+ def test_isoformat_datetime_scalar(self):
78
+ x = np.datetime64("2022-10-15T12:34:56.789")
79
+ assert dt.isoformat(x) == "2022-10-15T12:34:56.789000"
80
+
65
81
  def test_isoweek(self):
66
82
  a = dt.new(["2022-10-15", NaT])
67
83
  assert dt.isoweek(a).tolist() == [41, None]
@@ -508,6 +508,11 @@ class TestVector:
508
508
  assert a.str.str_len().tolist() == [4, 4]
509
509
  assert a.str.upper().tolist() == ["ASDF", "1234"]
510
510
 
511
+ def test_str_contains(self):
512
+ a = Vector(["asdf", "1234"])
513
+ assert a.str.contains("df").tolist() == [True, False]
514
+ assert a.str.contains("23").tolist() == [False, True]
515
+
511
516
  def test_tail(self):
512
517
  a = Vector([1, 2, 3, 4, 5])
513
518
  assert a.tail(3).tolist() == [3, 4, 5]
@@ -88,6 +88,7 @@ class StrProxy:
88
88
  self.add = wrap("add")
89
89
  self.capitalize = wrap("capitalize")
90
90
  self.center = wrap("center")
91
+ self.contains = lambda x: self.find(x) > -1
91
92
  self.count = wrap("count")
92
93
  self.decode = wrap("decode")
93
94
  self.encode = wrap("encode")
@@ -569,14 +570,6 @@ class Vector(np.ndarray):
569
570
  array = array.astype(dtypes.string)
570
571
  return array
571
572
 
572
- def _optimize_for_argsort(self):
573
- if self.is_string() and (n := self.str.str_len().max()) < 50:
574
- # XXX: We get a huge speed boost often by converting
575
- # to the old-style fixed-width strings! This is probably
576
- # temporary and can be removed once StringDType has matured.
577
- return self.astype(f"U{n}")
578
- return self
579
-
580
573
  def range(self):
581
574
  """
582
575
  Return the minimum and maximum values as a two-element vector.
@@ -614,7 +607,6 @@ class Vector(np.ndarray):
614
607
  if na.all():
615
608
  # Avoid trying to evaluate min/max/mean of all NA.
616
609
  self = self.fast(np.repeat(1, self.length))
617
- self = self._optimize_for_argsort()
618
610
  out = np.zeros_like(self, int)
619
611
  if method == "min":
620
612
  # https://stackoverflow.com/a/14672797/16369038
@@ -691,8 +683,8 @@ class Vector(np.ndarray):
691
683
  new = self.fast(lst, object)
692
684
  na = new.is_na()
693
685
  return new[~na].concat(new[na])
694
- opt = self._optimize_for_argsort()
695
- new = self[opt.argsort(kind="stable")]
686
+ new = self.copy()
687
+ np.ndarray.sort(new, kind="stable")
696
688
  if dir < 0:
697
689
  new = new[::-1]
698
690
  na = new.is_na()
@@ -859,6 +851,5 @@ class Vector(np.ndarray):
859
851
  >>> vector = di.Vector([1, 1, 1, 2, 2, 3])
860
852
  >>> vector.unique()
861
853
  """
862
- opt = self._optimize_for_argsort()
863
- u, indices = np.unique(opt, return_index=True)
854
+ u, indices = np.unique(self, return_index=True)
864
855
  return self[indices.sort()].copy()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes