Skill Tracer
Skills › Scientific Agent Skills › astropy › tables.md

tables.md

510 lines · skills/astropy/references/tables.md at 5f2bc03

Open on GitHub
1# Table Operations (astropy.table)
2
3The `astropy.table` module provides flexible tools for working with tabular data, with support for units, masked values, and various file formats.
4

Table Operations (astropy.table)

The astropy.table module provides flexible tools for working with tabular data, with support for units, masked values, and various file formats.

Raw lines 1 to 4
5## Creating Tables
6

Creating Tables

Raw lines 5 to 6
7### Basic Table Creation
8
9```python
10from astropy.table import Table, QTable
11import astropy.units as u
12import numpy as np
14# From column arrays
15a = [1, 4, 5]
16b = [2.0, 5.0, 8.2]
17c = ['x', 'y', 'z']
19t = Table([a, b, c], names=('id', 'flux', 'name'))
21# With units (use QTable)
22flux = [1.2, 2.3, 3.4] * u.Jy
23wavelength = [500, 600, 700] * u.nm
24t = QTable([flux, wavelength], names=('flux', 'wavelength'))
25```

Basic Table Creation

from astropy.table import Table, QTable
import astropy.units as u
import numpy as np

# From column arrays
a = [1, 4, 5]
b = [2.0, 5.0, 8.2]
c = ['x', 'y', 'z']

t = Table([a, b, c], names=('id', 'flux', 'name'))

# With units (use QTable)
flux = [1.2, 2.3, 3.4] * u.Jy
wavelength = [500, 600, 700] * u.nm
t = QTable([flux, wavelength], names=('flux', 'wavelength'))
Raw lines 7 to 26
27### From Lists of Rows
29```python
30# List of tuples
31rows = [(1, 10.5, 'A'), (2, 11.2, 'B'), (3, 12.3, 'C')]
32t = Table(rows=rows, names=('id', 'value', 'name'))
34# List of dictionaries
35rows = [{'id': 1, 'value': 10.5}, {'id': 2, 'value': 11.2}]
36t = Table(rows)
37```

From Lists of Rows

# List of tuples
rows = [(1, 10.5, 'A'), (2, 11.2, 'B'), (3, 12.3, 'C')]
t = Table(rows=rows, names=('id', 'value', 'name'))

# List of dictionaries
rows = [{'id': 1, 'value': 10.5}, {'id': 2, 'value': 11.2}]
t = Table(rows)
Raw lines 27 to 38
39### From NumPy Arrays
41```python
42# Structured array
43arr = np.array([(1, 2.0, 'x'), (4, 5.0, 'y')],
44 dtype=[('a', 'i4'), ('b', 'f8'), ('c', 'U10')])
45t = Table(arr)
47# 2D array with column names
48data = np.random.random((100, 3))
49t = Table(data, names=['col1', 'col2', 'col3'])
50```

From NumPy Arrays

# Structured array
arr = np.array([(1, 2.0, 'x'), (4, 5.0, 'y')],
               dtype=[('a', 'i4'), ('b', 'f8'), ('c', 'U10')])
t = Table(arr)

# 2D array with column names
data = np.random.random((100, 3))
t = Table(data, names=['col1', 'col2', 'col3'])
Raw lines 39 to 51
52### From Pandas DataFrame
54```python
55import pandas as pd
57df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
58t = Table.from_pandas(df)
59```

From Pandas DataFrame

import pandas as pd

df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
t = Table.from_pandas(df)
Raw lines 52 to 60
61## Accessing Table Data

Accessing Table Data

Raw lines 61 to 62
63### Basic Access
65```python
66# Column access
67ra_col = t['ra'] # Returns Column object
68dec_col = t['dec']
70# Row access
71first_row = t[0] # Returns Row object
72row_slice = t[10:20] # Returns new Table
74# Cell access
75value = t['ra'][5] # 6th value in 'ra' column
76value = t[5]['ra'] # Same thing
78# Multiple columns
79subset = t['ra', 'dec', 'mag']
80```

Basic Access

# Column access
ra_col = t['ra']           # Returns Column object
dec_col = t['dec']

# Row access
first_row = t[0]           # Returns Row object
row_slice = t[10:20]       # Returns new Table

# Cell access
value = t['ra'][5]         # 6th value in 'ra' column
value = t[5]['ra']         # Same thing

# Multiple columns
subset = t['ra', 'dec', 'mag']
Raw lines 63 to 81
82### Table Properties
84```python
85len(t) # Number of rows
86t.colnames # List of column names
87t.dtype # Column data types
88t.info # Detailed information
89t.meta # Metadata dictionary
90```

Table Properties

len(t)              # Number of rows
t.colnames          # List of column names
t.dtype             # Column data types
t.info              # Detailed information
t.meta              # Metadata dictionary
Raw lines 82 to 91
92### Iteration
94```python
95# Iterate over rows
96for row in t:
97 print(row['ra'], row['dec'])
99# Iterate over columns
100for colname in t.colnames:
101 print(t[colname])
102```

Iteration

# Iterate over rows
for row in t:
    print(row['ra'], row['dec'])

# Iterate over columns
for colname in t.colnames:
    print(t[colname])
Raw lines 92 to 103
104## Modifying Tables

Modifying Tables

Raw lines 104 to 105
106### Adding Columns
108```python
109# Add new column
110t['new_col'] = [1, 2, 3, 4, 5]
111t['calc'] = t['a'] + t['b'] # Calculated column
113# Add column with units
114t['velocity'] = [10, 20, 30] * u.km / u.s
116# Add empty column
117from astropy.table import Column
118t['empty'] = Column(length=len(t), dtype=float)
120# Insert at specific position
121t.add_column([7, 8, 9], name='inserted', index=2)
122```

Adding Columns

# Add new column
t['new_col'] = [1, 2, 3, 4, 5]
t['calc'] = t['a'] + t['b']  # Calculated column

# Add column with units
t['velocity'] = [10, 20, 30] * u.km / u.s

# Add empty column
from astropy.table import Column
t['empty'] = Column(length=len(t), dtype=float)

# Insert at specific position
t.add_column([7, 8, 9], name='inserted', index=2)
Raw lines 106 to 123
124### Removing Columns
126```python
127# Remove single column
128t.remove_column('old_col')
130# Remove multiple columns
131t.remove_columns(['col1', 'col2'])
133# Delete syntax
134del t['col_name']
136# Keep only specific columns
137t.keep_columns(['ra', 'dec', 'mag'])
138```

Removing Columns

# Remove single column
t.remove_column('old_col')

# Remove multiple columns
t.remove_columns(['col1', 'col2'])

# Delete syntax
del t['col_name']

# Keep only specific columns
t.keep_columns(['ra', 'dec', 'mag'])
Raw lines 124 to 139
140### Renaming Columns
142```python
143t.rename_column('old_name', 'new_name')
145# Rename multiple
146t.rename_columns(['old1', 'old2'], ['new1', 'new2'])
147```

Renaming Columns

t.rename_column('old_name', 'new_name')

# Rename multiple
t.rename_columns(['old1', 'old2'], ['new1', 'new2'])
Raw lines 140 to 148
149### Adding Rows
151```python
152# Add single row
153t.add_row([1, 2.5, 'new'])
155# Add row as dict
156t.add_row({'ra': 10.5, 'dec': 41.2, 'mag': 18.5})
158# Note: Adding rows one at a time is slow!
159# Better to collect rows and create table at once
160```

Adding Rows

# Add single row
t.add_row([1, 2.5, 'new'])

# Add row as dict
t.add_row({'ra': 10.5, 'dec': 41.2, 'mag': 18.5})

# Note: Adding rows one at a time is slow!
# Better to collect rows and create table at once
Raw lines 149 to 161
162### Modifying Data
164```python
165# Modify column values
166t['flux'] = t['flux'] * gain
167t['mag'][t['mag'] < 0] = np.nan
169# Modify single cell
170t['ra'][5] = 10.5
172# Modify entire row
173t[0] = [new_id, new_ra, new_dec]
174```

Modifying Data

# Modify column values
t['flux'] = t['flux'] * gain
t['mag'][t['mag'] < 0] = np.nan

# Modify single cell
t['ra'][5] = 10.5

# Modify entire row
t[0] = [new_id, new_ra, new_dec]
Raw lines 162 to 175
176## Sorting and Filtering

Sorting and Filtering

Raw lines 176 to 177
178### Sorting
180```python
181# Sort by single column
182t.sort('mag')
184# Sort descending
185t.sort('mag', reverse=True)
187# Sort by multiple columns
188t.sort(['priority', 'mag'])
190# Get sorted indices without modifying table
191indices = t.argsort('mag')
192sorted_table = t[indices]
193```

Sorting

# Sort by single column
t.sort('mag')

# Sort descending
t.sort('mag', reverse=True)

# Sort by multiple columns
t.sort(['priority', 'mag'])

# Get sorted indices without modifying table
indices = t.argsort('mag')
sorted_table = t[indices]
Raw lines 178 to 194
195### Filtering
197```python
198# Boolean indexing
199bright = t[t['mag'] < 18]
200nearby = t[t['distance'] < 100*u.pc]
202# Multiple conditions
203selected = t[(t['mag'] < 18) & (t['dec'] > 0)]
205# Using numpy functions
206high_snr = t[np.abs(t['flux'] / t['error']) > 5]
207```

Filtering

# Boolean indexing
bright = t[t['mag'] < 18]
nearby = t[t['distance'] < 100*u.pc]

# Multiple conditions
selected = t[(t['mag'] < 18) & (t['dec'] > 0)]

# Using numpy functions
high_snr = t[np.abs(t['flux'] / t['error']) > 5]
Raw lines 195 to 208
209## Reading and Writing Files

Reading and Writing Files

Raw lines 209 to 210
211### Supported Formats
213FITS, HDF5, ASCII (CSV, ECSV, IPAC, etc.), VOTable, Parquet. HDF5 needs h5py;
214Parquet needs PyArrow. ASDF integration is supplied by separate packages, not
215a guaranteed core `Table.read` format. Check `Table.read.list_formats()`.
216File-based fragments are illustrative recipes requiring the indicated inputs.

Supported Formats

FITS, HDF5, ASCII (CSV, ECSV, IPAC, etc.), VOTable, Parquet. HDF5 needs h5py; Parquet needs PyArrow. ASDF integration is supplied by separate packages, not a guaranteed core Table.read format. Check Table.read.list_formats(). File-based fragments are illustrative recipes requiring the indicated inputs.

Raw lines 211 to 217
218### Reading Files
220```python
221# Automatic format detection
222t = Table.read('catalog.fits')
223t = Table.read('data.csv')
224t = Table.read('table.vot')
226# Specify format explicitly
227t = Table.read('data.txt', format='ascii')
228t = Table.read('catalog.hdf5', path='/dataset/table')
230# Read specific HDU from FITS
231t = Table.read('file.fits', hdu=2)
232```

Reading Files

# Automatic format detection
t = Table.read('catalog.fits')
t = Table.read('data.csv')
t = Table.read('table.vot')

# Specify format explicitly
t = Table.read('data.txt', format='ascii')
t = Table.read('catalog.hdf5', path='/dataset/table')

# Read specific HDU from FITS
t = Table.read('file.fits', hdu=2)
Raw lines 218 to 233
234### Writing Files
236```python
237# Automatic format from extension
238t.write('output.fits')
239t.write('output.csv')
241# Specify format
242t.write('output.txt', format='ascii.csv')
243t.write('output.hdf5', path='/data/table', serialize_meta=True)
245# Overwrite existing file
246t.write('output.fits', overwrite=True)
247```

Writing Files

# Automatic format from extension
t.write('output.fits')
t.write('output.csv')

# Specify format
t.write('output.txt', format='ascii.csv')
t.write('output.hdf5', path='/data/table', serialize_meta=True)

# Overwrite existing file
t.write('output.fits', overwrite=True)
Raw lines 234 to 248
249### ASCII Format Options
251```python
252# CSV with custom delimiter
253t.write('output.csv', format='ascii.csv', delimiter='|')
255# Fixed-width format
256t.write('output.txt', format='ascii.fixed_width')
258# IPAC format
259t.write('output.tbl', format='ascii.ipac')
261# LaTeX table
262t.write('table.tex', format='ascii.latex')
263```
265For scientific round trips, prefer ECSV or an appropriate FITS representation
266over plain CSV. Compare units, masks, metadata and mixin columns after reading.
267Astropy 8 can persist indices with `write_indices=True` for ECSV/FITS/HDF5.
268FITS `Time` mixins should be read with `astropy_native=True`; choose
269`serialize_method={"flux": "data_mask", "time": "jd1_jd2"}` for a table with
270masked `flux` and `Time` columns. A global `"data_mask"` method is invalid for
271Time; choose per-column methods when types differ.

ASCII Format Options

# CSV with custom delimiter
t.write('output.csv', format='ascii.csv', delimiter='|')

# Fixed-width format
t.write('output.txt', format='ascii.fixed_width')

# IPAC format
t.write('output.tbl', format='ascii.ipac')

# LaTeX table
t.write('table.tex', format='ascii.latex')

For scientific round trips, prefer ECSV or an appropriate FITS representation over plain CSV. Compare units, masks, metadata and mixin columns after reading. Astropy 8 can persist indices with write_indices=True for ECSV/FITS/HDF5. FITS Time mixins should be read with astropy_native=True; choose serialize_method={"flux": "data_mask", "time": "jd1_jd2"} for a table with masked flux and Time columns. A global "data_mask" method is invalid for Time; choose per-column methods when types differ.

Raw lines 249 to 272
273## Table Operations

Table Operations

Raw lines 273 to 274
275### Stacking Tables (Vertical)
277```python
278from astropy.table import vstack
280# Concatenate tables vertically
281t1 = Table([[1, 2], [3, 4]], names=('a', 'b'))
282t2 = Table([[5, 6], [7, 8]], names=('a', 'b'))
283t_combined = vstack([t1, t2])
284```

Stacking Tables (Vertical)

from astropy.table import vstack

# Concatenate tables vertically
t1 = Table([[1, 2], [3, 4]], names=('a', 'b'))
t2 = Table([[5, 6], [7, 8]], names=('a', 'b'))
t_combined = vstack([t1, t2])
Raw lines 275 to 285
286### Joining Tables (Horizontal)
288```python
289from astropy.table import hstack
291# Concatenate tables horizontally
292t1 = Table([[1, 2]], names=['a'])
293t2 = Table([[3, 4]], names=['b'])
294t_combined = hstack([t1, t2])
295```

Joining Tables (Horizontal)

from astropy.table import hstack

# Concatenate tables horizontally
t1 = Table([[1, 2]], names=['a'])
t2 = Table([[3, 4]], names=['b'])
t_combined = hstack([t1, t2])
Raw lines 286 to 296
297### Database-Style Joins
299```python
300from astropy.table import join
302# Inner join on common column
303t1 = Table([[1, 2, 3], ['a', 'b', 'c']], names=('id', 'data1'))
304t2 = Table([[1, 2, 4], ['x', 'y', 'z']], names=('id', 'data2'))
305t_joined = join(t1, t2, keys='id')
307# Left/right/outer joins
308t_joined = join(t1, t2, join_type='left')
309t_joined = join(t1, t2, join_type='outer')
310```

Database-Style Joins

from astropy.table import join

# Inner join on common column
t1 = Table([[1, 2, 3], ['a', 'b', 'c']], names=('id', 'data1'))
t2 = Table([[1, 2, 4], ['x', 'y', 'z']], names=('id', 'data2'))
t_joined = join(t1, t2, keys='id')

# Left/right/outer joins
t_joined = join(t1, t2, join_type='left')
t_joined = join(t1, t2, join_type='outer')
Raw lines 297 to 311
312### Grouping and Aggregating
314```python
315# Group by column
316g = t.group_by('filter')
318# Aggregate groups
319means = g.groups.aggregate(np.mean)
321# Iterate over groups
322for group in g.groups:
323 print(f"Filter: {group['filter'][0]}")
324 print(f"Mean mag: {np.mean(group['mag'])}")
325```

Grouping and Aggregating

# Group by column
g = t.group_by('filter')

# Aggregate groups
means = g.groups.aggregate(np.mean)

# Iterate over groups
for group in g.groups:
    print(f"Filter: {group['filter'][0]}")
    print(f"Mean mag: {np.mean(group['mag'])}")
Raw lines 312 to 326
327### Unique Rows
329```python
330# Get unique rows
331from astropy.table import unique
332t_unique = unique(t, keys='id')
334# Multiple columns
335t_unique = unique(t, keys=['ra', 'dec'])
336```

Unique Rows

# Get unique rows
from astropy.table import unique
t_unique = unique(t, keys='id')

# Multiple columns
t_unique = unique(t, keys=['ra', 'dec'])
Raw lines 327 to 337
338## Units and Quantities
340Use QTable for unit-aware operations:
342```python
343from astropy.table import QTable
345# Create table with units
346t = QTable()
347t['flux'] = [1.2, 2.3, 3.4] * u.Jy
348t['wavelength'] = [500, 600, 700] * u.nm
350# Unit conversions
351t['flux'].to(u.mJy)
352t['wavelength'].to(u.angstrom)
354# Calculations preserve units
355t['freq'] = t['wavelength'].to(u.Hz, equivalencies=u.spectral())
356```

Units and Quantities

Use QTable for unit-aware operations:

from astropy.table import QTable

# Create table with units
t = QTable()
t['flux'] = [1.2, 2.3, 3.4] * u.Jy
t['wavelength'] = [500, 600, 700] * u.nm

# Unit conversions
t['flux'].to(u.mJy)
t['wavelength'].to(u.angstrom)

# Calculations preserve units
t['freq'] = t['wavelength'].to(u.Hz, equivalencies=u.spectral())
Raw lines 338 to 357
358## Masking Missing Data
360```python
361from astropy.table import MaskedColumn
362import numpy as np
364# Create masked column
365flux = MaskedColumn([1.2, np.nan, 3.4], mask=[False, True, False])
366t = Table([flux], names=['flux'])
368# Operations automatically handle masks
369mean_flux = np.ma.mean(t['flux'])
371# Fill masked values
372t['flux'].filled(0) # Replace masked with 0
373```

Masking Missing Data

from astropy.table import MaskedColumn
import numpy as np

# Create masked column
flux = MaskedColumn([1.2, np.nan, 3.4], mask=[False, True, False])
t = Table([flux], names=['flux'])

# Operations automatically handle masks
mean_flux = np.ma.mean(t['flux'])

# Fill masked values
t['flux'].filled(0)  # Replace masked with 0
Raw lines 358 to 374
375## Indexing for Fast Lookup
377Create indices for fast row retrieval:
379```python
380# Add index on column
381t.add_index('id')
383# Fast lookup by index
384row = t.loc[12345] # Find row where id=12345
386# Range queries
387subset = t.loc[100:200]
389# With multiple indices, select which index to use explicitly.
390# t.loc["b", 2] (index name as first element) is deprecated since 7.2
391# and slated for removal in 9.0.
392t.add_index('name')
393row = t.loc.with_index('name')['M31']
394```

Indexing for Fast Lookup

Create indices for fast row retrieval:

# Add index on column
t.add_index('id')

# Fast lookup by index
row = t.loc[12345]  # Find row where id=12345

# Range queries
subset = t.loc[100:200]

# With multiple indices, select which index to use explicitly.
# t.loc["b", 2] (index name as first element) is deprecated since 7.2
# and slated for removal in 9.0.
t.add_index('name')
row = t.loc.with_index('name')['M31']
Raw lines 375 to 395
396## Table Metadata
398```python
399# Set table-level metadata
400t.meta['TELESCOPE'] = 'HST'
401t.meta['FILTER'] = 'F814W'
402t.meta['EXPTIME'] = 300.0
404# Set column-level metadata
405t['ra'].unit = u.deg # Attach the actual column unit, not an arbitrary meta key
406t['ra'].description = 'Right Ascension' # Shortcut
407```

Table Metadata

# Set table-level metadata
t.meta['TELESCOPE'] = 'HST'
t.meta['FILTER'] = 'F814W'
t.meta['EXPTIME'] = 300.0

# Set column-level metadata
t['ra'].unit = u.deg  # Attach the actual column unit, not an arbitrary meta key
t['ra'].description = 'Right Ascension'  # Shortcut
Raw lines 396 to 408
409## Performance Tips

Performance Tips

Raw lines 409 to 410
411### Fast Table Construction
413```python
414# SLOW: Adding rows one at a time
415t = Table(names=['a', 'b'])
416for i in range(1000):
417 t.add_row([i, i**2])
419# FAST: Build from lists
420rows = [(i, i**2) for i in range(1000)]
421t = Table(rows=rows, names=['a', 'b'])
422```

Fast Table Construction

# SLOW: Adding rows one at a time
t = Table(names=['a', 'b'])
for i in range(1000):
    t.add_row([i, i**2])

# FAST: Build from lists
rows = [(i, i**2) for i in range(1000)]
t = Table(rows=rows, names=['a', 'b'])
Raw lines 411 to 423
424### Memory-Mapped FITS Tables
426```python
427# Memory mapping can reduce reads for compatible FITS columns
428t = Table.read('huge_catalog.fits', memmap=True)
430# String decoding, scaling and conversion can still materialize entire columns.
431subset = t[10000:10100]
432```

Memory-Mapped FITS Tables

# Memory mapping can reduce reads for compatible FITS columns
t = Table.read('huge_catalog.fits', memmap=True)

# String decoding, scaling and conversion can still materialize entire columns.
subset = t[10000:10100]
Raw lines 424 to 433
434### Copy vs. View
436```python
437# Row slices share ordinary column data.
438t_view = t[1:3]
440# Selecting multiple columns copies ordinary column data.
441t_copy = t['ra', 'dec']
442# Explicit independent copy of a row slice:
443t_independent = t[1:3].copy(copy_data=True)
444```

Copy vs. View

# Row slices share ordinary column data.
t_view = t[1:3]

# Selecting multiple columns copies ordinary column data.
t_copy = t['ra', 'dec']
# Explicit independent copy of a row slice:
t_independent = t[1:3].copy(copy_data=True)
Raw lines 434 to 445
446## Displaying Tables
448```python
449# Print to console
450print(t)
452# Show in interactive browser
453t.show_in_browser()
454t.show_in_browser(jsviewer=True) # Interactive sorting/filtering
456# Paginated viewing
457t.more()
459# Custom formatting
460t['flux'].format = '%.3f'
461t['ra'].format = '{:.6f}'
462```

Displaying Tables

# Print to console
print(t)

# Show in interactive browser
t.show_in_browser()
t.show_in_browser(jsviewer=True)  # Interactive sorting/filtering

# Paginated viewing
t.more()

# Custom formatting
t['flux'].format = '%.3f'
t['ra'].format = '{:.6f}'
Raw lines 446 to 463
464## Converting to Other Formats
466```python
467# To NumPy array
468arr = np.array(t)
470# To Pandas DataFrame; units, SkyCoord frame and Time scale/precision do not
471# round-trip as Astropy metadata. Keep an ECSV/FITS source for scientific reuse.
472df = t.to_pandas()
474# To dictionary
475d = {name: t[name] for name in t.colnames}
476```

Converting to Other Formats

# To NumPy array
arr = np.array(t)

# To Pandas DataFrame; units, SkyCoord frame and Time scale/precision do not
# round-trip as Astropy metadata. Keep an ECSV/FITS source for scientific reuse.
df = t.to_pandas()

# To dictionary
d = {name: t[name] for name in t.colnames}
Raw lines 464 to 477
478## Common Use Cases

Common Use Cases

Raw lines 478 to 479
480### Cross-Matching Catalogs
482```python
483from astropy.coordinates import SkyCoord, match_coordinates_sky
485# Create coordinate objects from table columns
486coords1 = SkyCoord(t1['ra'], t1['dec'], unit='deg')
487coords2 = SkyCoord(t2['ra'], t2['dec'], unit='deg')
489# Verify units, frame and reference epoch before matching; nearest neighbors
490# are candidate associations and may reuse the same catalog row.
491idx, sep, _ = coords1.match_to_catalog_sky(coords2)
493# Filter by separation
494max_sep = 1 * u.arcsec
495matches = sep < max_sep
496t1_matched = t1[matches]
497t2_matched = t2[idx[matches]]
498```

Cross-Matching Catalogs

from astropy.coordinates import SkyCoord, match_coordinates_sky

# Create coordinate objects from table columns
coords1 = SkyCoord(t1['ra'], t1['dec'], unit='deg')
coords2 = SkyCoord(t2['ra'], t2['dec'], unit='deg')

# Verify units, frame and reference epoch before matching; nearest neighbors
# are candidate associations and may reuse the same catalog row.
idx, sep, _ = coords1.match_to_catalog_sky(coords2)

# Filter by separation
max_sep = 1 * u.arcsec
matches = sep < max_sep
t1_matched = t1[matches]
t2_matched = t2[idx[matches]]
Raw lines 480 to 499
500### Binning Data
502```python
503from astropy.table import Table
504import numpy as np
506# Bin by magnitude
507mag_bins = np.arange(10, 20, 0.5)
508binned = t.group_by(np.digitize(t['mag'], mag_bins))
509counts = binned.groups.aggregate(len)
510```

Binning Data

from astropy.table import Table
import numpy as np

# Bin by magnitude
mag_bins = np.arange(10, 20, 0.5)
binned = t.group_by(np.digitize(t['mag'], mag_bins))
counts = binned.groups.aggregate(len)
Raw lines 500 to 510