nested-pandas 0.3.2__tar.gz → 0.3.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 (80) hide show
  1. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.copier-answers.yml +2 -1
  2. nested_pandas-0.3.3/.github/ISSUE_TEMPLATE/README.md +9 -0
  3. nested_pandas-0.3.3/.github/workflows/README.md +9 -0
  4. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/asv-main.yml +1 -1
  5. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/asv-nightly.yml +1 -1
  6. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/asv-pr.yml +1 -1
  7. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/build-documentation.yml +2 -2
  8. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/pre-commit-ci.yml +2 -2
  9. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/publish-benchmarks-pr.yml +1 -1
  10. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/publish-to-pypi.yml +1 -1
  11. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/smoke-test.yml +1 -1
  12. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/workflows/testing-and-coverage.yml +2 -2
  13. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.pre-commit-config.yaml +1 -1
  14. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.readthedocs.yml +1 -1
  15. {nested_pandas-0.3.2/src/nested_pandas.egg-info → nested_pandas-0.3.3}/PKG-INFO +1 -1
  16. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/benchmarks/asv.conf.json +1 -1
  17. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/index.rst +5 -0
  18. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/_version.py +2 -2
  19. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/nestedframe/core.py +117 -43
  20. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/series/packer.py +28 -4
  21. {nested_pandas-0.3.2 → nested_pandas-0.3.3/src/nested_pandas.egg-info}/PKG-INFO +1 -1
  22. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas.egg-info/SOURCES.txt +2 -0
  23. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/nestedframe/test_nestedframe.py +77 -0
  24. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/series/test_packer.py +36 -0
  25. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.git_archival.txt +0 -0
  26. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.gitattributes +0 -0
  27. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/ISSUE_TEMPLATE/0-general_issue.md +0 -0
  28. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/ISSUE_TEMPLATE/1-bug_report.md +0 -0
  29. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/ISSUE_TEMPLATE/2-feature_request.md +0 -0
  30. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/dependabot.yml +0 -0
  31. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.github/pull_request_template.md +0 -0
  32. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.gitignore +0 -0
  33. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/.setup_dev.sh +0 -0
  34. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/LICENSE +0 -0
  35. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/README.md +0 -0
  36. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/benchmarks/README.md +0 -0
  37. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/benchmarks/__init__.py +0 -0
  38. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/benchmarks/benchmarks.py +0 -0
  39. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/Makefile +0 -0
  40. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/_static/custom.css +0 -0
  41. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/conf.py +0 -0
  42. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/gettingstarted/contributing.rst +0 -0
  43. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/gettingstarted/installation.rst +0 -0
  44. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/gettingstarted/quickstart.ipynb +0 -0
  45. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/gettingstarted.rst +0 -0
  46. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/requirements.txt +0 -0
  47. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/tutorials/README.md +0 -0
  48. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/tutorials/data_loading_notebook.ipynb +0 -0
  49. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/tutorials/data_manipulation.ipynb +0 -0
  50. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/tutorials/low_level.ipynb +0 -0
  51. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/tutorials/nested_spectra.ipynb +0 -0
  52. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/docs/tutorials.rst +0 -0
  53. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/nestedframe.png +0 -0
  54. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/pyproject.toml +0 -0
  55. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/setup.cfg +0 -0
  56. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/__init__.py +0 -0
  57. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/datasets/__init__.py +0 -0
  58. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/datasets/generation.py +0 -0
  59. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/nestedframe/__init__.py +0 -0
  60. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/nestedframe/io.py +0 -0
  61. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/py.typed +0 -0
  62. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/series/__init__.py +0 -0
  63. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/series/accessor.py +0 -0
  64. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/series/dtype.py +0 -0
  65. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/series/ext_array.py +0 -0
  66. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/series/utils.py +0 -0
  67. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/utils/__init__.py +0 -0
  68. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas/utils/utils.py +0 -0
  69. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas.egg-info/dependency_links.txt +0 -0
  70. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas.egg-info/requires.txt +0 -0
  71. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/src/nested_pandas.egg-info/top_level.txt +0 -0
  72. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/conftest.py +0 -0
  73. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/datasets/test_generation.py +0 -0
  74. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/e2e_tests/test_issue89.py +0 -0
  75. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/nestedframe/test_io.py +0 -0
  76. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/series/test_accessor.py +0 -0
  77. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/series/test_dtype.py +0 -0
  78. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/series/test_ext_array.py +0 -0
  79. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/test_packaging.py +0 -0
  80. {nested_pandas-0.3.2 → nested_pandas-0.3.3}/tests/nested_pandas/utils/test_utils.py +0 -0
@@ -1,5 +1,5 @@
1
1
  # Changes here will be overwritten by Copier
2
- _commit: v2.0.3
2
+ _commit: v2.0.5
3
3
  _src_path: gh:lincc-frameworks/python-project-template
4
4
  author_email: brantd@uw.edu
5
5
  author_name: LINCC Frameworks
@@ -22,3 +22,4 @@ python_versions:
22
22
  - '3.10'
23
23
  - '3.11'
24
24
  - '3.12'
25
+ - '3.13'
@@ -0,0 +1,9 @@
1
+ # Configurations
2
+
3
+ Templates for various different issue types are defined in this directory
4
+ and a pull request template is defined as ``../pull_request_template.md``. Adding,
5
+ removing, and modifying these templates to suit the needs of your project is encouraged.
6
+
7
+ For more information about these templates, look here: https://lincc-ppt.readthedocs.io/en/latest/practices/issue_pr_templating.html
8
+
9
+ Or if you still have questions contact us: https://lincc-ppt.readthedocs.io/en/latest/source/contact.html
@@ -0,0 +1,9 @@
1
+ # Workflows
2
+
3
+ The .yml files in this directory are used to define the various continuous
4
+ integration scripts that will be run on your behalf e.g. nightly as a smoke check,
5
+ or when you create a new PR.
6
+
7
+ For more information about CI and workflows, look here: https://lincc-ppt.readthedocs.io/en/latest/practices/ci.html
8
+
9
+ Or if you still have questions contact us: https://lincc-ppt.readthedocs.io/en/latest/source/contact.html
@@ -8,7 +8,7 @@ on:
8
8
  branches: [ main ]
9
9
 
10
10
  env:
11
- PYTHON_VERSION: "3.10"
11
+ PYTHON_VERSION: "3.11"
12
12
  ASV_VERSION: "0.6.4"
13
13
  WORKING_DIR: ${{github.workspace}}/benchmarks
14
14
 
@@ -9,7 +9,7 @@ on:
9
9
  workflow_dispatch:
10
10
 
11
11
  env:
12
- PYTHON_VERSION: "3.10"
12
+ PYTHON_VERSION: "3.11"
13
13
  ASV_VERSION: "0.6.4"
14
14
  WORKING_DIR: ${{github.workspace}}/benchmarks
15
15
  NIGHTLY_HASH_FILE: nightly-hash
@@ -15,7 +15,7 @@ concurrency:
15
15
  cancel-in-progress: true
16
16
 
17
17
  env:
18
- PYTHON_VERSION: "3.10"
18
+ PYTHON_VERSION: "3.11"
19
19
  ASV_VERSION: "0.6.4"
20
20
  WORKING_DIR: ${{github.workspace}}/benchmarks
21
21
  ARTIFACTS_DIR: ${{github.workspace}}/artifacts
@@ -21,10 +21,10 @@ jobs:
21
21
 
22
22
  steps:
23
23
  - uses: actions/checkout@v4
24
- - name: Set up Python 3.10
24
+ - name: Set up Python 3.11
25
25
  uses: actions/setup-python@v5
26
26
  with:
27
- python-version: '3.10'
27
+ python-version: '3.11'
28
28
  - name: Install dependencies
29
29
  run: |
30
30
  sudo apt-get update
@@ -20,7 +20,7 @@ jobs:
20
20
  - name: Set up Python
21
21
  uses: actions/setup-python@v5
22
22
  with:
23
- python-version: '3.10'
23
+ python-version: '3.11'
24
24
  - name: Install dependencies
25
25
  run: |
26
26
  sudo apt-get update
@@ -32,5 +32,5 @@ jobs:
32
32
  extra_args: --all-files --verbose
33
33
  env:
34
34
  SKIP: "check-lincc-frameworks-template-version,no-commit-to-branch,check-added-large-files,validate-pyproject,sphinx-build,pytest-check"
35
- - uses: pre-commit-ci/lite-action@v1.0.3
35
+ - uses: pre-commit-ci/lite-action@v1.1.0
36
36
  if: failure() && github.event_name == 'pull_request' && github.event.pull_request.draft == false
@@ -27,7 +27,7 @@ jobs:
27
27
  echo "Conclusion: ${{ github.event.workflow_run.conclusion }}"
28
28
  echo "Event: ${{ github.event.workflow_run.event }}"
29
29
  - name: Download artifact
30
- uses: dawidd6/action-download-artifact@v6
30
+ uses: dawidd6/action-download-artifact@v7
31
31
  with:
32
32
  name: benchmark-artifacts
33
33
  run_id: ${{ github.event.workflow_run.id }}
@@ -27,7 +27,7 @@ jobs:
27
27
  - name: Set up Python
28
28
  uses: actions/setup-python@v5
29
29
  with:
30
- python-version: '3.10'
30
+ python-version: '3.11'
31
31
  - name: Install dependencies
32
32
  run: |
33
33
  python -m pip install --upgrade pip
@@ -20,7 +20,7 @@ jobs:
20
20
  runs-on: ubuntu-latest
21
21
  strategy:
22
22
  matrix:
23
- python-version: ['3.9', '3.10', '3.11', '3.12']
23
+ python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
24
24
 
25
25
  steps:
26
26
  - uses: actions/checkout@v4
@@ -15,7 +15,7 @@ jobs:
15
15
  runs-on: ubuntu-latest
16
16
  strategy:
17
17
  matrix:
18
- python-version: ['3.9', '3.10', '3.11', '3.12']
18
+ python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
19
19
 
20
20
  steps:
21
21
  - uses: actions/checkout@v4
@@ -33,6 +33,6 @@ jobs:
33
33
  run: |
34
34
  python -m pytest --cov=nested_pandas --cov-report=xml
35
35
  - name: Upload coverage report to codecov
36
- uses: codecov/codecov-action@v4
36
+ uses: codecov/codecov-action@v5
37
37
  with:
38
38
  token: ${{ secrets.CODECOV_TOKEN }}
@@ -18,7 +18,7 @@ repos:
18
18
  description: Clear output from Jupyter notebooks.
19
19
  files: \.ipynb$
20
20
  exclude: ^docs/pre_executed
21
- stages: [commit]
21
+ stages: [pre-commit]
22
22
  language: system
23
23
  entry: jupyter nbconvert --clear-output
24
24
  # Prevents committing directly branches named 'main' and 'master'.
@@ -9,7 +9,7 @@ version: 2
9
9
  build:
10
10
  os: ubuntu-22.04
11
11
  tools:
12
- python: "3.10"
12
+ python: "3.11"
13
13
 
14
14
  # Build documentation in the docs/ directory with Sphinx
15
15
  sphinx:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nested-pandas
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Author-email: LINCC Frameworks <brantd@uw.edu>
5
5
  License: MIT License
6
6
 
@@ -37,7 +37,7 @@
37
37
  // The Pythons you'd like to test against. If not provided, defaults
38
38
  // to the current version of Python used to run `asv`.
39
39
  "pythons": [
40
- "3.10"
40
+ "3.11"
41
41
  ],
42
42
  // The matrix of dependencies to test. Each key is the name of a
43
43
  // package (in PyPI) and the values are version numbers. An empty
@@ -19,10 +19,15 @@ is used internally to aid in scalability and performance.
19
19
  :align: center
20
20
  :alt: Example NestedFrame
21
21
 
22
+ <<<<<<< before updating
22
23
  Nested-Pandas is motivated by time-domain astronomy use cases, where we see
23
24
  typically two levels of information, information about astronomical objects and
24
25
  then an associated set of `N` measurements of those objects. Nested-Pandas offers
25
26
  a performant and memory-efficient package for working with these types of datasets.
27
+ =======
28
+ >> conda create env -n <env_name> python=3.11
29
+ >> conda activate <env_name>
30
+ >>>>>>> after updating
26
31
 
27
32
  Core advantages being:
28
33
 
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3.2'
16
- __version_tuple__ = version_tuple = (0, 3, 2)
15
+ __version__ = version = '0.3.3'
16
+ __version_tuple__ = version_tuple = (0, 3, 3)
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import ast
5
5
  import os
6
+ import re
6
7
 
7
8
  import numpy as np
8
9
  import pandas as pd
@@ -18,6 +19,10 @@ from pandas.core.computation.parsing import clean_column_name
18
19
  from nested_pandas.series.dtype import NestedDtype
19
20
  from nested_pandas.series.packer import pack, pack_lists, pack_sorted_df_into_struct
20
21
 
22
+ # Used to identify backtick-protected names in the expressions
23
+ # used in NestedFrame.eval() and NestedFrame.query().
24
+ _backtick_protected_names = re.compile(r"`[^`]+`", re.MULTILINE)
25
+
21
26
 
22
27
  class NestedPandasExprVisitor(PandasExprVisitor):
23
28
  """
@@ -83,9 +88,9 @@ class _NestResolver(dict):
83
88
  super().__init__()
84
89
  # Pre-load the field resolvers for all columns which are known at present.
85
90
  for column in outer.nested_columns:
86
- self._initialize_field_resolver(column, outer)
91
+ self._initialize_column_resolver(column, outer)
87
92
 
88
- def _initialize_field_resolver(self, column: str, outer: NestedFrame):
93
+ def _initialize_column_resolver(self, column: str, outer: NestedFrame):
89
94
  """
90
95
  Initialize a resolver for the given nested column, and also an alias
91
96
  for it, in the case of column names that have spaces or are otherwise
@@ -107,7 +112,7 @@ class _NestResolver(dict):
107
112
  if not super().__contains__(top_nest):
108
113
  if top_nest not in self._outer.nested_columns:
109
114
  raise KeyError(f"Unknown nest {top_nest}")
110
- self._initialize_field_resolver(top_nest, self._outer)
115
+ self._initialize_column_resolver(top_nest, self._outer)
111
116
  return super().__getitem__(top_nest)
112
117
 
113
118
  def __setitem__(self, item, _):
@@ -139,8 +144,20 @@ class _NestedFieldResolver:
139
144
  # Flattened only once for every access of this particular nest
140
145
  # within the expression.
141
146
  self._flat_nest = outer[nest_name].nest.to_flat()
147
+ # Save aliases to any columns that are not identifier-like.
148
+ # If our given frame has aliases for identifiers, use these instead
149
+ # of generating our own.
150
+ self._aliases = getattr(outer, "_aliases", None)
151
+ if self._aliases is None:
152
+ self._aliases = {}
153
+ for column in self._flat_nest.columns:
154
+ clean_id = clean_column_name(column)
155
+ if clean_id != column:
156
+ self._aliases[clean_id] = column
142
157
 
143
158
  def __getattr__(self, item_name: str):
159
+ if self._aliases:
160
+ item_name = self._aliases.get(item_name, item_name)
144
161
  if item_name in self._flat_nest:
145
162
  result = _SeriesFromNest(self._flat_nest[item_name])
146
163
  # Assigning these properties directly in order to avoid any complication
@@ -193,6 +210,25 @@ def _subexprs_by_nest(parents: list, node) -> dict[str, list]:
193
210
  return result
194
211
 
195
212
 
213
+ def _identify_aliases(expr: str) -> tuple[str, dict[str, str]]:
214
+ """
215
+ Given an expression string, identify backtick-quoted names
216
+ and replace them with cleaned names, returning the cleaned
217
+ expression and a dictionary of aliases, where the keys are
218
+ clean aliases to the original names.
219
+ """
220
+ aliases = {}
221
+
222
+ def sub_and_alias(match):
223
+ original = match.group(0)[1:-1] # remove backticks
224
+ alias = clean_column_name(original)
225
+ if alias != original:
226
+ aliases[alias] = original
227
+ return alias
228
+
229
+ return _backtick_protected_names.sub(sub_and_alias, expr), aliases
230
+
231
+
196
232
  class NestedFrame(pd.DataFrame):
197
233
  """A Pandas Dataframe extension with support for nested structure.
198
234
 
@@ -205,6 +241,12 @@ class NestedFrame(pd.DataFrame):
205
241
  # Series produce instances of this class, preserving the type and origin.
206
242
  __pandas_priority__ = 4500
207
243
 
244
+ # The "_aliases" attribute is usually None or not even present, but when it is present,
245
+ # it indicates that an evaluation is in progress, and that columns and fields with names
246
+ # that are not identifier-like have been aliases to cleaned names, and this attribute
247
+ # contains those aliases, keyed by the cleaned name.
248
+ _metadata = ["_aliases"]
249
+
208
250
  @property
209
251
  def _constructor(self) -> Self: # type: ignore[name-defined] # noqa: F821
210
252
  return NestedFrame
@@ -232,62 +274,86 @@ class NestedFrame(pd.DataFrame):
232
274
  nest_cols.append(column)
233
275
  return nest_cols
234
276
 
235
- def _is_known_hierarchical_column(self, colname) -> bool:
277
+ def _parse_hierarchical_components(self, delimited_path: str, delimiter: str = ".") -> list[str]:
278
+ """
279
+ Given a string that may be a delimited path, parse it into its components,
280
+ respecting backticks that are used to protect component names that may contain the delimiter.
281
+ """
282
+ aliases = getattr(self, "_aliases", None)
283
+ if aliases is None:
284
+ delimited_path, aliases = _identify_aliases(delimited_path)
285
+ return [aliases.get(x, x) for x in delimited_path.split(delimiter)]
286
+
287
+ def _is_known_hierarchical_column(self, components: list[str] | str) -> bool:
236
288
  """Determine whether a string is a known hierarchical column name"""
237
- if "." in colname:
238
- base_name = colname.split(".")[0]
239
- if base_name in self.nested_columns:
240
- # TODO: only handles one level of nesting for now
241
- nested_name = ".".join(colname.split(".")[1:])
242
- return nested_name in self.all_columns[base_name]
289
+ if isinstance(components, str):
290
+ components = self._parse_hierarchical_components(components)
291
+ if len(components) < 2:
243
292
  return False
293
+ base_name = components[0]
294
+ if base_name in self.nested_columns:
295
+ nested_name = ".".join(components[1:])
296
+ return nested_name in self.all_columns[base_name]
244
297
  return False
245
298
 
246
- def _is_known_column(self, colname) -> bool:
247
- """Determine whether a string is a known column name"""
248
- return colname in self.columns or self._is_known_hierarchical_column(colname)
299
+ def _is_known_column(self, components: list[str] | str) -> bool:
300
+ """Determine whether a list of field components describes a known column name"""
301
+ if isinstance(components, str):
302
+ components = self._parse_hierarchical_components(components)
303
+ if ".".join(components) in self.columns:
304
+ return True
305
+ return self._is_known_hierarchical_column(components)
249
306
 
250
307
  def __getitem__(self, item):
251
308
  """Adds custom __getitem__ behavior for nested columns"""
252
309
 
253
- if isinstance(item, str):
254
- # Preempt the nested check if the item is a base column
255
- if item in self.columns:
256
- return super().__getitem__(item)
257
- # If a nested column name is passed, return a flat series for that column
258
- # flat series is chosen over list series for utility
259
- # e.g. native ability to do something like ndf["nested.a"] + 3
260
- elif self._is_known_hierarchical_column(item):
261
- # TODO: only handles one level of nesting for now
262
- nested = item.split(".")[0]
263
- col = ".".join(item.split(".")[1:])
264
- return self[nested].nest.get_flat_series(col)
265
- else:
266
- raise KeyError(f"Column '{item}' not found in nested columns or base columns")
267
- else:
310
+ if not isinstance(item, str):
268
311
  return super().__getitem__(item)
269
312
 
313
+ # Preempt the nested check if the item is a base column, with or without
314
+ # dots and backticks.
315
+ if item in self.columns:
316
+ return super().__getitem__(item)
317
+ components = self._parse_hierarchical_components(item)
318
+ # One more check on the entirety of the item name, in case backticks were used
319
+ # (even if they weren't necessary).
320
+ cleaned_item = ".".join(components)
321
+ if cleaned_item in self.columns:
322
+ return super().__getitem__(cleaned_item)
323
+
324
+ # If a nested column name is passed, return a flat series for that column
325
+ # flat series is chosen over list series for utility
326
+ # e.g. native ability to do something like ndf["nested.a"] + 3
327
+ if self._is_known_hierarchical_column(components):
328
+ nested = components[0]
329
+ field = ".".join(components[1:])
330
+ return self[nested].nest.get_flat_series(field)
331
+ else:
332
+ raise KeyError(f"Column '{cleaned_item}' not found in nested columns or base columns")
333
+
270
334
  def __setitem__(self, key, value):
271
335
  """Adds custom __setitem__ behavior for nested columns"""
272
-
336
+ components = self._parse_hierarchical_components(key)
273
337
  # Replacing or adding columns to a nested structure
274
338
  # Allows statements like ndf["nested.t"] = ndf["nested.t"] - 5
275
339
  # Or ndf["nested.base_t"] = ndf["nested.t"] - 5
276
340
  # Performance note: This requires building a new nested structure
277
341
  # TODO: Support assignment of a new column to an existing nested col from a list series
278
- if self._is_known_hierarchical_column(key) or (
279
- "." in key and key.split(".")[0] in self.nested_columns
342
+ if self._is_known_hierarchical_column(components) or (
343
+ len(components) > 1 and components[0] in self.nested_columns
280
344
  ):
281
- nested, col = key.split(".")
282
- new_nested_series = self[nested].nest.with_flat_field(col, value)
345
+ if len(components) != 2:
346
+ raise ValueError(f"Only one level of nesting is supported; given {key}")
347
+ nested, field = components
348
+ new_nested_series = self[nested].nest.with_flat_field(field, value)
283
349
  return super().__setitem__(nested, new_nested_series)
284
350
 
285
351
  # Adding a new nested structure from a column
286
352
  # Allows statements like ndf["new_nested.t"] = ndf["nested.t"] - 5
287
- if "." in key:
288
- new_nested, col = key.split(".")
353
+ if len(components) > 1:
354
+ new_nested, field = components
289
355
  if isinstance(value, pd.Series):
290
- value.name = col
356
+ value.name = field
291
357
  value = value.to_frame()
292
358
  new_df = self.add_nested(value, name=new_nested)
293
359
  self._update_inplace(new_df)
@@ -512,10 +578,15 @@ class NestedFrame(pd.DataFrame):
512
578
  --------
513
579
  https://pandas.pydata.org/docs/reference/api/pandas.eval.html
514
580
  """
581
+ _, aliases = _identify_aliases(expr)
582
+ self._aliases: dict[str, str] | None = aliases
583
+
515
584
  kwargs["resolvers"] = tuple(kwargs.get("resolvers", ())) + (_NestResolver(self),)
516
585
  kwargs["inplace"] = inplace
517
586
  kwargs["parser"] = "nested-pandas"
518
- return super().eval(expr, **kwargs)
587
+ answer = super().eval(expr, **kwargs)
588
+ self._aliases = None
589
+ return answer
519
590
 
520
591
  def extract_nest_names(
521
592
  self,
@@ -801,7 +872,7 @@ class NestedFrame(pd.DataFrame):
801
872
  Takes a function and applies it to each top-level row of the NestedFrame.
802
873
 
803
874
  The user may specify which columns the function is applied to, with
804
- columns from the 'base' layer being passsed to the function as
875
+ columns from the 'base' layer being passed to the function as
805
876
  scalars and columns from the nested layers being passed as numpy arrays.
806
877
 
807
878
  Parameters
@@ -838,12 +909,15 @@ class NestedFrame(pd.DataFrame):
838
909
  # Parse through the initial args to determine the columns to apply the function to
839
910
  requested_columns = []
840
911
  for arg in args:
841
- if not isinstance(arg, str) or not self._is_known_column(arg):
842
- # We've reached an argument that is not a valid column, so we assume
843
- # the remaining args are extra arguments to the function
912
+ # Stop when we reach an argument that is not a valid column, as we assume
913
+ # that the remaining args are extra arguments to the function
914
+ if not isinstance(arg, str):
915
+ break
916
+ components = self._parse_hierarchical_components(arg)
917
+ if not self._is_known_column(components):
844
918
  break
845
- layer = "base" if "." not in arg else arg.split(".")[0]
846
- col = arg.split(".")[-1]
919
+ layer = "base" if len(components) < 2 else components[0]
920
+ col = components[-1]
847
921
  requested_columns.append((layer, col))
848
922
 
849
923
  # We require the first *args to be the columns to apply the function to
@@ -196,10 +196,34 @@ def pack_lists(df: pd.DataFrame, name: str | None = None, *, validate: bool = Tr
196
196
  nested_pandas.series.dtype.NestedDtype : The dtype of the output series.
197
197
  nested_pandas.series.packer.pack_flat : Pack a "flat" dataframe with repeated indexes.
198
198
  """
199
- struct_array = pa.StructArray.from_arrays(
200
- [df[column] for column in df.columns],
201
- names=df.columns,
202
- )
199
+ # When series is converted to pa.array it may be both Array and ChunkedArray
200
+ # We convert it to chunked for the sake of consistency
201
+ pa_arrays_maybe_chunked = {column: pa.array(df[column]) for column in df.columns}
202
+ pa_chunked_arrays = {
203
+ column: arr if isinstance(arr, pa.ChunkedArray) else pa.chunked_array([arr])
204
+ for column, arr in pa_arrays_maybe_chunked.items()
205
+ }
206
+
207
+ # If all chunk arrays have the same chunk lengths, we can build a chunked struct array with no
208
+ # data copying.
209
+ chunk_lengths = pa.array([[len(chunk) for chunk in arr.chunks] for arr in pa_chunked_arrays.values()])
210
+ if all(chunk_length == chunk_lengths[0] for chunk_length in chunk_lengths):
211
+ chunks = []
212
+ numpy_chunks = next(iter(pa_chunked_arrays.values())).num_chunks
213
+ for i in range(numpy_chunks):
214
+ chunks.append(
215
+ pa.StructArray.from_arrays(
216
+ [arr.chunk(i) for arr in pa_chunked_arrays.values()],
217
+ names=pa_chunked_arrays.keys(),
218
+ )
219
+ )
220
+ struct_array = pa.chunked_array(chunks)
221
+ else: # "flatten" the chunked arrays
222
+ struct_array = pa.StructArray.from_arrays(
223
+ [arr.combine_chunks() for arr in pa_chunked_arrays.values()],
224
+ names=pa_chunked_arrays.keys(),
225
+ )
226
+
203
227
  ext_array = NestedExtensionArray(struct_array, validate=validate)
204
228
  return pd.Series(
205
229
  ext_array,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nested-pandas
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Author-email: LINCC Frameworks <brantd@uw.edu>
5
5
  License: MIT License
6
6
 
@@ -14,6 +14,8 @@ pyproject.toml
14
14
  .github/ISSUE_TEMPLATE/0-general_issue.md
15
15
  .github/ISSUE_TEMPLATE/1-bug_report.md
16
16
  .github/ISSUE_TEMPLATE/2-feature_request.md
17
+ .github/ISSUE_TEMPLATE/README.md
18
+ .github/workflows/README.md
17
19
  .github/workflows/asv-main.yml
18
20
  .github/workflows/asv-nightly.yml
19
21
  .github/workflows/asv-pr.yml
@@ -85,6 +85,35 @@ def test_is_known_hierarchical_column():
85
85
  assert not base._is_known_hierarchical_column("base.a")
86
86
 
87
87
 
88
+ def test_is_known_column():
89
+ """
90
+ Test that known (non-hierarchical) columns can be identified. The key
91
+ point to test is that columns which might look like they are nested,
92
+ but which are already known to not be, are correctly identified.
93
+ """
94
+ base = NestedFrame(data={"R. A.": [1, 2, 3], "nested.b": [2, 4, 6]}, index=[0, 1, 2])
95
+ nested = pd.DataFrame(
96
+ data={"c": [0, 2, 4, 1, 4, 3, 1, 4, 1], "d": [5, 4, 7, 5, 3, 1, 9, 3, 4]},
97
+ index=[0, 0, 0, 1, 1, 1, 2, 2, 2],
98
+ )
99
+
100
+ base = base.add_nested(nested, "nested")
101
+
102
+ assert base._is_known_column("R. A.")
103
+ assert base._is_known_column("`R. A.`")
104
+ assert base._is_known_column("nested.b")
105
+
106
+ # In this context, the "." delimiter matters a lot, so the following, which would be
107
+ # acceptable in an .eval() context, is not acceptable here.
108
+ assert not base._is_known_column("nested . b")
109
+ assert not base._is_known_column("nested. c")
110
+ assert not base._is_known_column("nested .d")
111
+
112
+ # But hierarchical ones should also work
113
+ assert base._is_known_column("nested.c")
114
+ assert base._is_known_column("nested.d")
115
+
116
+
88
117
  def test_get_nested_column():
89
118
  """Test that __getitem__ can retrieve a nested column"""
90
119
 
@@ -167,6 +196,21 @@ def test_get_dot_names():
167
196
  assert len(nf["nested.R.A."]) == 4
168
197
 
169
198
 
199
+ def test_nesting_limit():
200
+ """Test the ability to prevent nesting beyond a depth of 1."""
201
+ nf = NestedFrame.from_flat(
202
+ NestedFrame({"a": [1, 2, 3, 4], ".b.": [1, 1, 3, 3], "R.A.": [3, None, 6, 5]}, index=[1, 1, 2, 2]),
203
+ base_columns=[".b."],
204
+ )
205
+ with pytest.raises(ValueError):
206
+ # The error gets triggered for the attempt to create new nested columns; if the column has
207
+ # already been created, it should be fine.
208
+ nf["nested.c.d.e"] = nf[".b."]
209
+ nf["nested.c"] = nf["nested.R.A."]
210
+ # Test that the above works with backticks, too, even in cases where they are not strictly necessary.
211
+ nf["`nested.d`"] = nf["`.b.`"]
212
+
213
+
170
214
  def test_add_nested_with_flat_df():
171
215
  """Test that add_nested correctly adds a nested column to the base df"""
172
216
 
@@ -612,6 +656,17 @@ def test_query_on_non_identifier_columns():
612
656
  nf3 = nf.query("`bad dog`.a > 2")
613
657
  assert nf3["bad dog"].nest["a"].size == 4
614
658
 
659
+ # And also for fields within the nested columns.
660
+ # Taken from GH#176
661
+ nf = NestedFrame(data={"dog": [1, 2, 3], "good dog": [2, 4, 6]}, index=[0, 1, 2])
662
+ nested = pd.DataFrame(
663
+ data={"n/a": [0, 2, 4, 1, 4, 3, 1, 4, 1], "n/b": [5, 4, 7, 5, 3, 1, 9, 3, 4]},
664
+ index=[0, 0, 0, 1, 1, 1, 2, 2, 2],
665
+ )
666
+ nf = nf.add_nested(nested, "bad dog")
667
+ nf4 = nf.query("`bad dog`.`n/a` > 2")
668
+ assert nf4["bad dog"].nest["n/a"].size == 4
669
+
615
670
 
616
671
  def test_dropna():
617
672
  """Test that dropna works on all layers"""
@@ -815,6 +870,14 @@ def test_reduce():
815
870
  for i in range(len(result)):
816
871
  assert result["offset_avg"].values[i] == expected_offset_avg[i]
817
872
 
873
+ # Verify that we can understand a string argument to the reduce function,
874
+ # so long as it isn't a column name.
875
+ def make_id(col1, prefix_str):
876
+ return f"{prefix_str}{col1}"
877
+
878
+ result = nf.reduce(make_id, "b", "some_id_")
879
+ assert result[0][1] == "some_id_4"
880
+
818
881
 
819
882
  def test_reduce_duplicated_cols():
820
883
  """Tests nf.reduce() to correctly handle duplicated column names."""
@@ -1034,6 +1097,20 @@ def test_eval_assignment():
1034
1097
  assert (nf["p2.e"] == nf["packed.d"] * 2 + nf.c).all()
1035
1098
  assert (nf["p2.f"] == nf["p2.e"] + nf.b).all()
1036
1099
 
1100
+ # Verify that assignment can be done to nested columns and fields
1101
+ # having names which are not valid Python identifiers, and must
1102
+ # be quoted with backticks.
1103
+ nf = NestedFrame(data={"dog": [1, 2, 3], "good dog": [2, 4, 6]}, index=[0, 1, 2])
1104
+ nested = pd.DataFrame(
1105
+ data={"n/a": [0, 2, 4, 1, 4, 3, 1, 4, 1], "n/b": [5, 4, 7, 5, 3, 1, 9, 3, 4]},
1106
+ index=[0, 0, 0, 1, 1, 1, 2, 2, 2],
1107
+ )
1108
+ nf = nf.add_nested(nested, "bad dog")
1109
+ nfx = nf.eval("`bad dog`.`n/c` = `bad dog`.`n/b` + 2.5")
1110
+ # The number of columns at the top should not have changed
1111
+ assert len(nfx.columns) == len(nf.columns)
1112
+ assert (nfx["bad dog"].nest["n/c"] == nf["bad dog"].nest["n/b"] + 2.5).all()
1113
+
1037
1114
 
1038
1115
  def test_access_non_existing_column():
1039
1116
  """Test that accessing a non-existing column raises a KeyError"""
@@ -286,6 +286,42 @@ def test_pack_lists():
286
286
  assert_series_equal(series.nest.get_list_series(field_name), packed_df[field_name])
287
287
 
288
288
 
289
+ def test_pack_lists_with_chunked_arrays():
290
+ """Issue https://github.com/lincc-frameworks/nested-pandas/issues/180"""
291
+ chunked_a = pd.Series(
292
+ pa.chunked_array([pa.array([[1, 2, 3], [4, 5]])] * 3),
293
+ dtype=pd.ArrowDtype(pa.list_(pa.int64())),
294
+ name="a",
295
+ )
296
+ chunked_b = pd.Series(
297
+ pa.chunked_array([pa.array([[0.0, 1.0, 2.0], [3.0, 4.0]])] * 3),
298
+ dtype=pd.ArrowDtype(pa.list_(pa.float64())),
299
+ name="b",
300
+ )
301
+ list_df = pd.DataFrame({"a": chunked_a, "b": chunked_b}, index=[0, 1, 2, 3, 4, 5])
302
+ series = packer.pack_lists(list_df)
303
+ assert_series_equal(series.nest.get_list_series("a"), chunked_a)
304
+ assert_series_equal(series.nest.get_list_series("b"), chunked_b)
305
+
306
+
307
+ def test_pack_lists_with_uneven_chunked_arrays():
308
+ """Issue https://github.com/lincc-frameworks/nested-pandas/issues/180"""
309
+ chunked_a = pd.Series(
310
+ pa.chunked_array([pa.array([[1, 2, 3], [4, 5]])] * 3),
311
+ dtype=pd.ArrowDtype(pa.list_(pa.int64())),
312
+ name="a",
313
+ )
314
+ chunked_b = pd.Series(
315
+ pa.array([[0.0, 1.0, 2.0], [3.0, 4.0]] * 3),
316
+ dtype=pd.ArrowDtype(pa.list_(pa.float64())),
317
+ name="b",
318
+ )
319
+ list_df = pd.DataFrame({"a": chunked_a, "b": chunked_b}, index=[0, 1, 2, 3, 4, 5])
320
+ series = packer.pack_lists(list_df)
321
+ assert_series_equal(series.nest.get_list_series("a"), chunked_a)
322
+ assert_series_equal(series.nest.get_list_series("b"), chunked_b)
323
+
324
+
289
325
  def test_pack_seq_with_dfs_and_index():
290
326
  """Test pack_seq()."""
291
327
  dfs = [
File without changes
File without changes
File without changes
File without changes