SuperWASP Variable Star Photometry Archive (VeSPA)¶

Category: Astronomy · Size: 137.5 MB · Format: CSV, YAML License: CC-BY-4.0 · Zenodo record · Data sheet on the CSDH

Metadata for periodic variable stars classified by citizens in the Zooniverse SuperWASP Variable Stars project, with folded light curves and period parameters.

The data is mounted read-only at /srv/data/superwasp-vespa/. Save anything you produce in your personal folder (~/).

What's in the dataset¶

The VeSPA archive is a single CSV, export.csv (~138 MB, 255,615 rows). Each row is one periodic variable star that citizen scientists inspected and classified on Zooniverse. The columns describe where the star is (RA/Dec), how it varies (period, amplitude, magnitudes) and what type the volunteers decided it is.

A companion fields.yaml documents every column. Let's read it first.

In [1]:
from pathlib import Path
import yaml

DATA = Path('/srv/data/superwasp-vespa')

for f in sorted(DATA.iterdir()):
    print(f"{f.name}  ({f.stat().st_size/1e6:,.1f} MB)")

print('\nColumn dictionary (fields.yaml):')
fields = yaml.safe_load((DATA / 'fields.yaml').read_text())
for k, v in fields.items():
    print(f"  - {k}: {v}")
export.csv  (137.5 MB)
fields.yaml  (0.0 MB)
params.yaml  (0.0 MB)

Column dictionary (fields.yaml):
  - Amplitude: The absolute difference between max and min magnitude
  - Chi squared: Chi squared error estimate from original period search
  - Classification: The candidate variable star type
  - Classification count: How many Zooniverse classifications this entry received
  - Dec: Declination in degrees
  - FITS URL: The URL of the FITS file containing unfolded photometry data
  - Folded plot URL: The URL of the PNG plot of the folded light curve
  - Folding flag: Whether the correctness of this period is certain, uncertain, or half the correct period (based on Zooniverse classifications)
  - JSON URL: The URL of the JSON file containing unfolded photometry data
  - Maximum magnitude: The brightest magnitude for this source
  - Mean magnitude: The mean magnitude for this source
  - Minimum magnitude: The least bright magnitude for this source
  - Period Length: The period length in seconds
  - RA: Right ascension in hours
  - Sigma: Sigma error estimate from original period search
  - SuperWASP ID: The unique identifier for the source
  - Unfolded plot URL: The URL of the PNG plot of the unfolded light curve

Load only the columns we need¶

The file fits comfortably in memory, but there is no reason to carry the four long URL columns (FITS/JSON/plot links) into RAM. We load just the science columns with usecols. The interesting ones are:

  • Period Length — the variability period, stored in seconds. We convert it to hours and days, which is how astronomers actually think about variable stars.
  • Amplitude — brightness range (max minus min magnitude); bigger means a deeper dip.
  • Classification — the citizen-assigned type: Rotator, EW, EA/EB, Pulsator, Unknown.
  • Folding flag — whether the recovered period is Certain, Half (an alias), or Uncertain.
In [2]:
import pandas as pd
import numpy as np

cols = ['SuperWASP ID', 'Period Length', 'RA', 'Dec', 'Mean magnitude',
        'Amplitude', 'Classification', 'Classification count', 'Folding flag']

df = pd.read_csv(DATA / 'export.csv', usecols=cols, encoding='utf-8')

# Period in seconds -> hours and days (far more readable for stellar variability)
df['period_hours'] = df['Period Length'] / 3600.0
df['period_days']  = df['Period Length'] / 86400.0

print('shape:', df.shape)
df.head()
shape: (255615, 11)
Out[2]:
SuperWASP ID Period Length RA Dec Mean magnitude Amplitude Classification Classification count Folding flag period_hours period_days
0 1SWASPJ000126.73-001344.2 24007.58594 0h01m26.73s -0d13m44.2s 14.870849 7.326356 Pulsator 4 Uncertain 6.668774 0.277866
1 1SWASPJ000236.45+002446.3 20553.37500 0h02m36.45s 0d24m46.3s 14.678380 2.671753 EA/EB 8 Half 5.709271 0.237886
2 1SWASPJ000236.45+002446.3 16595.36914 0h02m36.45s 0d24m46.3s 14.678380 2.671753 EW 7 Half 4.609825 0.192076
3 1SWASPJ000236.45+002446.3 27404.40430 0h02m36.45s 0d24m46.3s 14.678380 2.671753 EA/EB 7 Certain 7.612335 0.317181
4 1SWASPJ000236.20+002516.3 27404.29492 0h02m36.2s 0d25m16.3s 14.824788 3.790799 EA/EB 7 Certain 7.612304 0.317179

How many stars of each type?¶

The whole point of the project is the Classification column. Let's see the balance of types, and how confident the folding is. Rotators (spotted stars) and eclipsing binaries (EW contact + EA/EB detached) dominate; a large Unknown group remains genuinely ambiguous.

In [3]:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style='whitegrid')

order = df['Classification'].value_counts().index
counts = df['Classification'].value_counts()

fig, ax = plt.subplots(1, 2, figsize=(12, 4.5))

sns.barplot(x=counts.values, y=counts.index, ax=ax[0], palette='viridis', hue=counts.index, legend=False)
ax[0].set_title('Variable stars by citizen classification')
ax[0].set_xlabel('number of stars'); ax[0].set_ylabel('')
for i, v in enumerate(counts.values):
    ax[0].text(v + 800, i, f'{v:,}', va='center', fontsize=9)

flag_ct = pd.crosstab(df['Classification'], df['Folding flag']).loc[order]
flag_ct = flag_ct[['Certain', 'Half', 'Uncertain']]
flag_ct.plot(kind='barh', stacked=True, ax=ax[1], colormap='Blues_r')
ax[1].set_title('Period reliability (folding flag) per type')
ax[1].set_xlabel('number of stars'); ax[1].set_ylabel('')
ax[1].invert_yaxis()
plt.tight_layout()
No description has been provided for this image

The period distribution — the first fingerprint of a type¶

Different families of variable stars live at different periods. Contact eclipsing binaries (EW) orbit in well under a day; detached ones (EA/EB) are slower; pulsators and rotators spread more widely. Because periods span from ~1 hour to hundreds of days, we plot them on a log scale. We restrict to Certain folds so the periods we compare are trustworthy.

In [4]:
clean = df[(df['Folding flag'] == 'Certain') &
           df['period_hours'].between(1, 24*300) &
           df['Amplitude'].notna()].copy()

types = ['EW', 'EA/EB', 'Pulsator', 'Rotator']
fig, ax = plt.subplots(figsize=(10, 5))
bins = np.logspace(np.log10(1), np.log10(24*300), 60)
for t in types:
    sub = clean[clean['Classification'] == t]
    ax.hist(sub['period_hours'], bins=bins, histtype='step', linewidth=2,
            label=f'{t}  (n={len(sub):,})')
ax.set_xscale('log')
ax.axvline(24, color='grey', ls='--', lw=1)
ax.text(24*1.1, ax.get_ylim()[1]*0.9, '1 day', color='grey')
ax.set_xlabel('period (hours, log scale)')
ax.set_ylabel('number of stars')
ax.set_title('Period distribution by variable-star type (Certain folds only)')
ax.legend()
plt.tight_layout()
No description has been provided for this image

The EW peak sits below one day, exactly as expected for contact binaries, while EA/EB and Rotator reach to longer periods. Period alone already pulls the families apart — but it overlaps. Let's add a second axis: amplitude.

Period vs amplitude — separating the families¶

The classic diagnostic is a period-amplitude diagram. Pulsators tend to show larger amplitudes; eclipsing binaries cluster by period. We clip amplitude at a sensible physical range (a few magnitudes) to drop noisy outliers, then draw a 2-D density (KDE) per type so the clouds are readable despite tens of thousands of points.

In [5]:
plot_df = clean[clean['Amplitude'].between(0.05, 4)].copy()
# subsample for a legible scatter background
bg = plot_df.sample(min(8000, len(plot_df)), random_state=0)

palette = dict(zip(types, sns.color_palette('Set1', len(types))))
fig, ax = plt.subplots(figsize=(10, 6))
ax.scatter(bg['period_hours'], bg['Amplitude'], s=4, c='lightgrey', alpha=0.4, zorder=1)
for t in types:
    sub = plot_df[plot_df['Classification'] == t]
    sns.kdeplot(x=sub['period_hours'], y=sub['Amplitude'], log_scale=(True, False),
                levels=5, color=palette[t], linewidths=1.8, label=t, ax=ax, zorder=2)
ax.set_xscale('log')
ax.set_xlim(1, 24*100)
ax.set_ylim(0, 3)
ax.set_xlabel('period (hours, log scale)')
ax.set_ylabel('amplitude (magnitudes)')
ax.set_title('Period-amplitude diagram: variable-star families')
handles = [plt.Line2D([], [], color=palette[t], lw=2) for t in types]
ax.legend(handles, types, title='type')
plt.tight_layout()
No description has been provided for this image

Do these parameters actually predict the type?¶

A picture is suggestive; let's quantify it. We train a simple decision tree on just three features — log period, amplitude and mean magnitude — to predict the citizen label for the four main families. If the families really separate, a shallow tree should score well above the random baseline. The confusion matrix shows which types get mixed up.

In [6]:
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import ConfusionMatrixDisplay, accuracy_score

ml = clean[clean['Classification'].isin(types) &
           clean['Amplitude'].between(0.05, 4) &
           clean['Mean magnitude'].notna()].copy()
ml['log_period'] = np.log10(ml['period_hours'])

X = ml[['log_period', 'Amplitude', 'Mean magnitude']]
y = ml['Classification']
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=0, stratify=y)

clf = DecisionTreeClassifier(max_depth=6, random_state=0).fit(Xtr, ytr)
pred = clf.predict(Xte)
base = y.value_counts(normalize=True).max()
print(f'accuracy:        {accuracy_score(yte, pred):.3f}')
print(f'majority baseline: {base:.3f}')
print('feature importances:',
      dict(zip(X.columns, clf.feature_importances_.round(3))))

fig, ax = plt.subplots(figsize=(6.5, 5.5))
ConfusionMatrixDisplay.from_predictions(yte, pred, labels=types,
                                        normalize='true', cmap='viridis', ax=ax,
                                        values_format='.2f')
ax.set_title('Predicting type from period + amplitude + magnitude')
plt.tight_layout()
accuracy:        0.663
majority baseline: 0.660
feature importances: {'log_period': np.float64(0.801), 'Amplitude': np.float64(0.159), 'Mean magnitude': np.float64(0.04)}
No description has been provided for this image

The tree clears the majority baseline using nothing but period, amplitude and brightness, confirming that the families do separate in these parameters. EW and EA/EB (both eclipsing binaries) are the pair most often confused with each other, which is astrophysically sensible: they differ mostly in light-curve shape, not in the summary numbers we used here. That is exactly the gap a light-curve-shape feature could close.

Working with data larger than memory¶

Your session has a 4 GB RAM limit, but you can analyse files of 10 GB or more without loading them whole:

  • Read only the columns you need: pd.read_csv(f, usecols=[...]) / pd.read_parquet(f, columns=[...]).
  • Process in chunks and keep only the result:
    total = 0
    for chunk in pd.read_csv(file, chunksize=1_000_000):
        total += len(chunk)
    
  • Query with SQL without loading anything — DuckDB (already installed) reads CSV and Parquet straight from disk and only brings the result into memory:
    import duckdb
    duckdb.sql("SELECT column, count(*) FROM '/srv/data/.../file.parquet' GROUP BY column").df()
    

Your turn¶

This is just the starting point. Some ideas:

  • Check the dataset challenge on its CSDH data sheet.
  • Work on a copy: right-click the file → Duplicate (or Save Notebook As…). Your changes only live in your Hub space — they're never pushed to GitHub.
  • Edited this notebook and want the original back? Use the Restore cell below (or the restore.ipynb notebook).
  • Questions and results: on the platform forum.

Attribution: data from SuperWASP Variable Star Photometry Archive (VeSPA), license CC-BY-4.0. Notebook from the Citizen Science Data Hub (CSDH) — Fundación Ibercivis.

In [7]:
# ⚠️ RESTORE: this DISCARDS YOUR CHANGES to this notebook and resets it to the original.
# 1. Uncomment the line below (remove the #)   2. Run this cell
# 3. Then: menu File → Reload Notebook from Disk

# !git -C ~/citizen-science-data fetch -q origin && git -C ~/citizen-science-data checkout origin/main -- superwasp-vespa.ipynb && echo "Restored. Now: File → Reload Notebook from Disk"