dataiter 1.0__tar.gz → 1.2__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.0 → dataiter-1.2}/.gitignore +1 -0
  2. {dataiter-1.0 → dataiter-1.2}/PKG-INFO +2 -2
  3. {dataiter-1.0 → dataiter-1.2}/dataiter/__init__.py +1 -1
  4. {dataiter-1.0 → dataiter-1.2}/dataiter/aggregate.py +9 -16
  5. {dataiter-1.0 → dataiter-1.2}/dataiter/data_frame.py +71 -3
  6. {dataiter-1.0 → dataiter-1.2}/dataiter/dt.py +13 -0
  7. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_aggregate.py +2 -0
  8. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_data_frame.py +35 -0
  9. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_dt.py +16 -0
  10. {dataiter-1.0 → dataiter-1.2}/pyproject.toml +1 -1
  11. {dataiter-1.0 → dataiter-1.2}/AUTHORS.md +0 -0
  12. {dataiter-1.0 → dataiter-1.2}/COPYING +0 -0
  13. {dataiter-1.0 → dataiter-1.2}/README.md +0 -0
  14. {dataiter-1.0 → dataiter-1.2}/dataiter/deco.py +0 -0
  15. {dataiter-1.0 → dataiter-1.2}/dataiter/dtypes.py +0 -0
  16. {dataiter-1.0 → dataiter-1.2}/dataiter/geojson.py +0 -0
  17. {dataiter-1.0 → dataiter-1.2}/dataiter/io.py +0 -0
  18. {dataiter-1.0 → dataiter-1.2}/dataiter/list_of_dicts.py +0 -0
  19. {dataiter-1.0 → dataiter-1.2}/dataiter/regex.py +0 -0
  20. {dataiter-1.0 → dataiter-1.2}/dataiter/test/__init__.py +0 -0
  21. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_geojson.py +0 -0
  22. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_io.py +0 -0
  23. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_list_of_dicts.py +0 -0
  24. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_regex.py +0 -0
  25. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_util.py +0 -0
  26. {dataiter-1.0 → dataiter-1.2}/dataiter/test/test_vector.py +0 -0
  27. {dataiter-1.0 → dataiter-1.2}/dataiter/util.py +0 -0
  28. {dataiter-1.0 → dataiter-1.2}/dataiter/vector.py +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,10 +1,10 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dataiter
3
- Version: 1.0
3
+ Version: 1.2
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>
7
- License-Expression: MIT
7
+ License: MIT
8
8
  License-File: AUTHORS.md
9
9
  License-File: COPYING
10
10
  Requires-Python: >=3.9.0
@@ -25,7 +25,7 @@ import numpy as np
25
25
 
26
26
  from dataiter import util
27
27
 
28
- __version__ = "1.0"
28
+ __version__ = "1.2"
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
  """
@@ -776,6 +781,69 @@ class DataFrame(dict):
776
781
  def _parse_rows_from_integer(self, rows):
777
782
  return Vector.fast(rows, int)
778
783
 
784
+ def pivot_longer(self, *, ids=None, names=None, values=None):
785
+ """
786
+ Pivot data from wide to long format.
787
+
788
+ `ids` should be the name or a list of names of identifier columns. All
789
+ other columns are considered variable columns and will be pivoted.
790
+ `names` and `values` are names of columns into which the variable names
791
+ and values are put in the result.
792
+
793
+ >>> wide = di.read_csv("data/listings.csv")
794
+ >>> wide
795
+ >>> wide.pivot_longer(ids="id", names="name", values="value")
796
+ """
797
+ ids = util.sequencify(ids)
798
+ if not all(x in self for x in ids):
799
+ raise ValueError(f"Bad {ids=}")
800
+ long = self.__class__()
801
+ for name in (x for x in self if x not in ids):
802
+ part = self.select(*ids)
803
+ part[names] = name
804
+ # Maybe we could avoid using object here,
805
+ # but it gets complicated.
806
+ part[values] = self[name].as_object()
807
+ long = long.rbind(part)
808
+ return long.sort(**dict.fromkeys(ids, 1))
809
+
810
+ def pivot_wider(self, *, ids=None, names=None, values=None, rename=None):
811
+ """
812
+ Pivot data from long to wide format.
813
+
814
+ `ids` should be the name or a list of names of identifier columns by
815
+ which the result will be unique. If `ids` is not given, it defaults to
816
+ all columns except `names` and `values`. `names` should be the name of
817
+ the column that contains the names of variables, which become new
818
+ columns. `values` should be the name of the column that contains the
819
+ corresponding values. `rename` is an optional function that you can use
820
+ to e.g. lowercase the new column names or add a prefix or suffix.
821
+
822
+ >>> long = di.read_csv("data/downloads.csv")
823
+ >>> long = long.sort(date=1)
824
+ >>> long
825
+ >>> long.pivot_wider(ids="date", names="category", values="downloads", rename=str.lower)
826
+ """
827
+ if names not in self:
828
+ raise ValueError(f"Bad {names=}")
829
+ if values not in self:
830
+ raise ValueError(f"Bad {values=}")
831
+ if not ids:
832
+ ids = [x for x in self if x not in [names, values]]
833
+ ids = util.sequencify(ids)
834
+ if not all(x in self for x in ids):
835
+ raise ValueError(f"Bad {ids=}")
836
+ long = self.copy()
837
+ wide = self.select(*ids).unique(*ids)
838
+ if rename:
839
+ long[names] = long[names].map(rename)
840
+ for name in long[names].unique().sort():
841
+ part = long.filter(long[names] == name)
842
+ part = part.select(*ids, values)
843
+ part = part.rename(**{name: values})
844
+ wide = wide.left_join(part, *ids)
845
+ return wide
846
+
779
847
  def pop(self, key, *args, **kwargs):
780
848
  """"""
781
849
  value = super().pop(key, *args, **kwargs)
@@ -1195,9 +1263,9 @@ class DataFrame(dict):
1195
1263
  batch_rows[i] += " "
1196
1264
  batch_rows[i] += column[i]
1197
1265
  del columns[colname]
1198
- rows_to_print.append("" if rows_to_print else ".")
1266
+ rows_to_print.append("" if rows_to_print else "")
1199
1267
  rows_to_print += batch_rows
1200
- rows_to_print.append(".")
1268
+ rows_to_print.append("")
1201
1269
  if max_rows < self.nrow:
1202
1270
  rows_to_print.append(f"... {self.nrow} rows total")
1203
1271
  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`.
@@ -171,6 +171,8 @@ class TestAggregate:
171
171
  assert stat.a.equal(expected)
172
172
  except AssertionError:
173
173
  print("")
174
+ print("Function:", function.__name__)
175
+ print("Numba:", use_numba)
174
176
  print(data)
175
177
  print("Expected:")
176
178
  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()
@@ -375,6 +384,32 @@ class TestDataFrame:
375
384
  data = test.data_frame("vehicles.csv")
376
385
  assert data.nrow == 33442
377
386
 
387
+ def test_pivot_longer(self):
388
+ wide = test.data_frame("listings.csv")
389
+ wide = wide.sort(id=1).head(10)
390
+ long = wide.pivot_longer(ids="id",
391
+ names="name",
392
+ values="value")
393
+
394
+ assert long.nrow == 50
395
+ assert long.ncol == 3
396
+ assert all(np.isin(long.name, wide.colnames))
397
+
398
+ def test_pivot_wider(self):
399
+ long = test.data_frame("downloads.csv")
400
+ long = long.sort(date=1).tail(100)
401
+ wide = long.pivot_wider(ids="date",
402
+ names="category",
403
+ values="downloads",
404
+ rename=lambda x: f"dl_{x.lower()}")
405
+
406
+ assert wide.nrow == 20
407
+ assert wide.ncol == 6
408
+ total_downloads = sum(
409
+ sum(wide[x]) for x in wide.colnames
410
+ if x.startswith("dl_"))
411
+ assert total_downloads == sum(long.downloads)
412
+
378
413
  def test_pop(self):
379
414
  data = test.data_frame("vehicles.csv")
380
415
  data.pop("fuel")
@@ -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]
@@ -7,7 +7,7 @@ name = "dataiter"
7
7
  dynamic = ["version"]
8
8
  description = "Simple, light-weight data frames for Python"
9
9
  readme = "README.md"
10
- license = "MIT"
10
+ license = { text = "MIT" }
11
11
  requires-python = ">=3.9.0"
12
12
  authors = [{ name = "Osmo Salomaa", email = "otsaloma@iki.fi" }]
13
13
  dependencies = ["attd>=0.3", "numpy>=2.0,<3.0", "pyarrow>=2.0", "wcwidth>=0.1"]
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
File without changes
File without changes