dataframe-toolkit 0.9.0__tar.gz → 0.9.1__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 (48) hide show
  1. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/PKG-INFO +16 -1
  2. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/README.md +15 -0
  3. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/pyproject.toml +1 -1
  4. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/pyproject.toml.orig +1 -1
  5. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/merge_cmd.py +48 -0
  6. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/common/io.py +27 -13
  7. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/__init__.py +0 -0
  8. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/cli.py +0 -0
  9. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/__init__.py +0 -0
  10. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/annotate_cmd.py +0 -0
  11. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/base.py +0 -0
  12. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/binx_cmd.py +0 -0
  13. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/chi_cmd.py +0 -0
  14. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/clean_cmd.py +0 -0
  15. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/cleannames_cmd.py +0 -0
  16. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/concat_cmd.py +0 -0
  17. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/corr_cmd.py +0 -0
  18. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/dataset_cmd.py +0 -0
  19. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/describe_cmd.py +0 -0
  20. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/eval_cmd.py +0 -0
  21. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/fit_cmd.py +0 -0
  22. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/func_cmd.py +0 -0
  23. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/help.py +0 -0
  24. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/hist_cmd.py +0 -0
  25. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/info_cmd.py +0 -0
  26. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/interp_cmd.py +0 -0
  27. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/line_cmd.py +0 -0
  28. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/melt_cmd.py +0 -0
  29. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/pivot_cmd.py +0 -0
  30. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/print_cmd.py +0 -0
  31. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/query_cmd.py +0 -0
  32. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/randvar_cmd.py +0 -0
  33. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/sample_cmd.py +0 -0
  34. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/scale_cmd.py +0 -0
  35. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/scat_cmd.py +0 -0
  36. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/segid_cmd.py +0 -0
  37. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/split_cmd.py +0 -0
  38. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/stat_cmd.py +0 -0
  39. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/test_cmd.py +0 -0
  40. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/transpose_cmd.py +0 -0
  41. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/commands/wstat_cmd.py +0 -0
  42. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/common/__init__.py +0 -0
  43. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/common/copula.py +0 -0
  44. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/common/plot.py +0 -0
  45. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/common/seed.py +0 -0
  46. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/common/seq.py +0 -0
  47. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/common/stats.py +0 -0
  48. {dataframe_toolkit-0.9.0 → dataframe_toolkit-0.9.1}/src/dftk/io.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dataframe-toolkit
3
- Version: 0.9.0
3
+ Version: 0.9.1
4
4
  Summary: A command-line toolkit for DataFrame analysis and manipulation
5
5
  Keywords: dataframe,statistics,data analysis,CLI,TSV,pandas
6
6
  Author: Gunnar Pálsson
@@ -251,6 +251,21 @@ All commands accept:
251
251
  > parquet and prints *its path* to stdout — `-` would try to parse that path
252
252
  > as TSV and fail.
253
253
 
254
+ ### Comment lines and preambles
255
+
256
+ dftk has no comment or skip-rows option: `#` is ordinary data, so headers
257
+ like `Count #` and values like `A #1` are read as they are. Drop comment
258
+ lines or a preamble in the shell instead:
259
+
260
+ ```bash
261
+ grep -v '^#' data.tsv | dftk print # drop every line starting with #
262
+ tail -n +3 data.tsv | dftk print # skip a 2-line preamble
263
+ dftk print <(grep -v '^#' data.tsv) # the same, as a filename
264
+ ```
265
+
266
+ With `--backend duckdb`, a whole-line `#` comment below the header is an
267
+ error, and `#` lines above the header are skipped.
268
+
254
269
  Standard output options (available on all tabular commands):
255
270
 
256
271
  - `-o` / `--output` — controls where output goes; takes an *optional* value:
@@ -222,6 +222,21 @@ All commands accept:
222
222
  > parquet and prints *its path* to stdout — `-` would try to parse that path
223
223
  > as TSV and fail.
224
224
 
225
+ ### Comment lines and preambles
226
+
227
+ dftk has no comment or skip-rows option: `#` is ordinary data, so headers
228
+ like `Count #` and values like `A #1` are read as they are. Drop comment
229
+ lines or a preamble in the shell instead:
230
+
231
+ ```bash
232
+ grep -v '^#' data.tsv | dftk print # drop every line starting with #
233
+ tail -n +3 data.tsv | dftk print # skip a 2-line preamble
234
+ dftk print <(grep -v '^#' data.tsv) # the same, as a filename
235
+ ```
236
+
237
+ With `--backend duckdb`, a whole-line `#` comment below the header is an
238
+ error, and `#` lines above the header are skipped.
239
+
225
240
  Standard output options (available on all tabular commands):
226
241
 
227
242
  - `-o` / `--output` — controls where output goes; takes an *optional* value:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.9.0"
3
+ version = "0.9.1"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  keywords = [
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dataframe-toolkit"
3
- version = "0.9.0"
3
+ version = "0.9.1"
4
4
  description = "A command-line toolkit for DataFrame analysis and manipulation"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -17,6 +17,8 @@ Key features
17
17
  * --select [:right:] expands to all right-file columns, suppressing ``_l``.
18
18
  * Both input files can be TSV or Parquet (.parquet extension auto-detected).
19
19
  The left file defaults to stdin, making it parquet-pipe friendly.
20
+ * Integer columns that a left, right or outer join leaves gaps in become
21
+ nullable (Int64) instead of float64.
20
22
  """
21
23
 
22
24
  import argparse
@@ -107,6 +109,35 @@ def _add_cross_col(
107
109
  args.drop = drop
108
110
 
109
111
 
112
+ def _gap_sides(args: argparse.Namespace) -> tuple[bool, bool]:
113
+ """Return (left, right): which side's columns can get missing values.
114
+
115
+ --only runs an outer join before filtering, so both sides count.
116
+ """
117
+ return args.type in ("right", "outer"), args.type in ("left", "outer")
118
+
119
+
120
+ def _nullable_ints(df: pd.DataFrame, keep: list[str]) -> pd.DataFrame:
121
+ """Return *df* with numpy integer columns (except *keep*) as Int64 etc.
122
+
123
+ A plain int64 column cannot hold a missing value, so pandas turns it into
124
+ float64 when a join leaves rows unmatched; the nullable type keeps the
125
+ integers.
126
+ """
127
+ cols = [
128
+ c
129
+ for c in df.columns
130
+ if c not in keep
131
+ and pd.api.types.is_integer_dtype(df[c])
132
+ and not isinstance(df[c].dtype, pd.api.extensions.ExtensionDtype)
133
+ ]
134
+ if not cols:
135
+ return df
136
+ # int32 -> Int32, uint8 -> UInt8, ...
137
+ dtypes = {c: df[c].dtype.name.capitalize().replace("Uint", "UInt") for c in cols}
138
+ return df.astype(dtypes)
139
+
140
+
110
141
  def _filter_only(
111
142
  df: pd.DataFrame,
112
143
  df_left: pd.DataFrame,
@@ -152,6 +183,15 @@ def _do_merge(
152
183
 
153
184
  suffixes = _expand_select(list(df_left.columns), list(df_right.columns), args)
154
185
 
186
+ # Keep integer columns integers when the join leaves gaps in them. The
187
+ # -k keys are filled from both sides, so they never get gaps.
188
+ keys = args.keys or []
189
+ left_gaps, right_gaps = _gap_sides(args)
190
+ if left_gaps:
191
+ df_left = _nullable_ints(df_left, keys)
192
+ if right_gaps:
193
+ df_right = _nullable_ints(df_right, keys)
194
+
155
195
  if args.keys is not None:
156
196
  df_merged = pd.merge(
157
197
  df_left,
@@ -278,6 +318,14 @@ would be read as if it were the data.
278
318
  Named-file self-merge (no stdin required):
279
319
 
280
320
  dftk merge penguins.tsv -r penguins_adelie.tsv -k species --only left
321
+
322
+ INTEGER COLUMNS
323
+ ---------------
324
+ A left, right or outer join leaves the other side's columns empty in
325
+ unmatched rows. Integer columns there are made nullable (Int64), so they
326
+ stay integers (100, not 100.0); the -k key columns are not changed. Read
327
+ TSV input with --backend duckdb to keep integer columns that already have
328
+ empty cells as integers too.
281
329
  """
282
330
 
283
331
 
@@ -27,6 +27,7 @@ import fnmatch
27
27
  import io as _io
28
28
  import logging
29
29
  import os
30
+ import shutil
30
31
  import sys
31
32
  import tempfile
32
33
 
@@ -365,14 +366,30 @@ class io:
365
366
  return _name_columns(df)
366
367
 
367
368
  def _csv_with_duckdb(path: str) -> pd.DataFrame:
368
- # DuckDB keeps integer columns with blanks as Int64 (pandas: float64)
369
+ # DuckDB keeps integer columns with blanks as Int64 (pandas: float64).
370
+ # comment="" stops it guessing "#" as a comment character: after one
371
+ # whole-line "#" comment it cuts every line at "#", headers included
369
372
  try:
370
- df = duckdb.read_csv(path, sep=sep, header=not noheader).df()
373
+ df = duckdb.read_csv(
374
+ path, sep=sep, header=not noheader, comment=""
375
+ ).df()
371
376
  except Exception as exc:
372
377
  logging.error(f"DuckDB failed to read {path!r}: {exc}")
373
378
  raise
374
379
  return _name_columns(df)
375
380
 
381
+ def _spooled_csv_with_duckdb(stream) -> pd.DataFrame:
382
+ # DuckDB reads its input twice (type sniffing, then data), so a
383
+ # stream must be copied to a temp file first
384
+ os.makedirs(DFTK_TMPDIR, exist_ok=True)
385
+ fd, path = tempfile.mkstemp(suffix=".tsv", dir=DFTK_TMPDIR)
386
+ try:
387
+ with os.fdopen(fd, "wb") as fh:
388
+ shutil.copyfileobj(stream, fh)
389
+ return _csv_with_duckdb(path)
390
+ finally:
391
+ os.unlink(path)
392
+
376
393
  def _csv_from_file(path: str) -> pd.DataFrame:
377
394
  df = _pandas_csv(path)
378
395
  df = _with_blank_lines(
@@ -403,21 +420,18 @@ class io:
403
420
  df = pd.read_parquet(filename)
404
421
  df.attrs["_parquet_meta"] = _read_parquet_meta(filename)
405
422
  elif backend == "duckdb" and duckdb is not None:
406
- df = _csv_with_duckdb(filename)
423
+ if os.path.exists(filename) and not os.path.isfile(filename):
424
+ # A pipe, e.g. <(grep -v '^#' file): one read would use it up
425
+ with open(filename, "rb") as fh:
426
+ df = _spooled_csv_with_duckdb(fh)
427
+ else:
428
+ df = _csv_with_duckdb(filename)
407
429
  else:
408
430
  df = _csv_from_file(filename)
409
431
 
410
432
  elif backend == "duckdb" and duckdb is not None:
411
- # stdin with DuckDB: it reads files, not buffers (that needs fsspec),
412
- # so spool stdin to a temp file and read it like a named file
413
- os.makedirs(DFTK_TMPDIR, exist_ok=True)
414
- fd, path = tempfile.mkstemp(suffix=".tsv", dir=DFTK_TMPDIR)
415
- try:
416
- with os.fdopen(fd, "wb") as fh:
417
- fh.write(sys.stdin.buffer.read())
418
- df = _csv_with_duckdb(path)
419
- finally:
420
- os.unlink(path)
433
+ # stdin with DuckDB: it reads files, not buffers (that needs fsspec)
434
+ df = _spooled_csv_with_duckdb(sys.stdin.buffer)
421
435
 
422
436
  else:
423
437
  # stdin — read TSV directly