Obscura

Hacker News Stories

Front-page stories from Hacker News.

hn_stories — the dataset name to pass to the Obscura API.

Category Web attention Columns 12 Refresh Daily Point-in-time available_date

What one row means

One daily snapshot of a single Hacker News story's front-page rank/score/ comment-count as observed on one calendar day of collection: the collector fetches topstories.json plus each item document once per run and upserts on (story_id, captured_date), so a story that stays on the front page for several days produces one row per day, each carrying that day's refreshed score/comments/rank.

One row per (story_id, captured_date).

Point-in-time availability

Every Obscura dataset carries available_date: the calendar day the publisher made the row available, day-of, with no session rounding. It is the one column a backtest filters on, and it means the same thing on every dataset in the catalog.

For hn_stories: published: available_date = captured_date, the UTC calendar day the collector's snapshot run captured this row (STORED generated column, generated from captured_date itself since migration 000175 — previously synced_at::date, which is the same day on a live run but the REPLAY day on a bronze replay).

Refresh cadence

Obscura refreshes hn_stories daily — the most frequent scheduled job that re-collects or re-exports it. This is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when a row became public is recorded per row in available_date.

Schema — 12 columns

The full public column list for hn_stories, with the meaning of every field. The same schema is served unauthenticated at https://api.obscura.trade/v1/catalog/hn_stories.

ColumnTypeDescription
story_idbigint · not nullHacker News Firebase item id for the story (e.g. 48905028); stable across all daily snapshots of the same story. First half of the composite primary key. Sourced from the Firebase topstories.json id list and confirmed via item/{id}.json's 'id' field (parse_item).
captured_datedate · not nullUTC calendar day (Utc::now().date_naive() at task start) on which this snapshot run captured the story's rank/score/comments. Second half of the composite primary key/OnConflict target; this is the column the ORM upserts on, and is the true public-availability day for this row's data — the day this specific score/comment/rank snapshot became knowable.
rankinteger · not null1-based position of the story within the top_n slice of topstories.json at capture time (i=index+1 in hackernews.rs::run); updated on every re-capture (part of the upsert's update_columns), so it reflects rank as of captured_date, not the story's rank history.
titletextStory headline as submitted to HN, from the item document's 'title' field; refreshed on every re-capture (the upsert replaces every column the batch writes), so it reflects the title as of the LAST capture on captured_date — an HN moderator retitle lands here.
scoreintegerHN points/upvote score at capture time, from the item document's 'score' field; refreshed on every re-capture (in update_columns) so each day's row shows that day's current score, the primary attention/virality signal for this dataset.
commentsintegerTotal descendant comment count at capture time, from the item document's 'descendants' field (parse_item maps descendants -> comments); nullable because Ask HN / some item types omit descendants. Refreshed on every re-capture.
authortextHN username of the story submitter, from the item document's 'by' field. NOT refreshed on upsert — captured only at first snapshot of the story.
urltextExternal URL the story links to, from the item document's 'url' field; nullable/absent for text-only submissions such as Ask HN posts (parse_item leaves it None when the field is missing). NOT refreshed on upsert.
event_timetimestamp with time zoneThe story's original HN submission instant — the real-world event — parsed from the item document's unix 'time' field (DateTime::from_timestamp). Constant across every daily snapshot row for the same story_id (submission never changes); this is the period/event moment, distinct from available_date which tracks when each day's snapshot became knowable.
item_typetextHacker News item kind from the item document's 'type' field — `story` / `job` / `poll` / `ask` (etc.). topstories.json mixes real stories with job and poll posts, so this is the field that lets a consumer separate a job/poll row from a genuine story. Captured at first snapshot (item type never changes).
texttextHTML body of a text-only submission, from the item document's 'text' field — the entire post content for Ask HN / Show HN text posts / job posts, where 'url' is null. Null for link submissions. Not derivable from any other kept field; captured at first snapshot.
available_datedatePUBLIC-availability date = captured_date. STORED generated column (migration 000175), read-only; the point-in-time column to filter/join on — the UTC day this specific snapshot row's rank/score/comments became knowable, NOT the story's original submission day.

Access hn_stories

Two delivery paths, one identifier. Both require an Obscura account and an active subscription; the catalog entry and the schema above are public.

import obscura

client = obscura.Client("obs_live_…")

df = client.query(
    dataset="hn_stories",
    start="2024-01-01",
)

Create a free account Browse all 95 datasets

Frequently asked questions

What is in the hn_stories dataset?

Front-page stories from Hacker News. One daily snapshot of a single Hacker News story's front-page rank/score/ comment-count as observed on one calendar day of collection: the collector fetches topstories.json plus each item document once per run and upserts on (story_id, captured_date), so a story that stays on the front page for several days produces one row per day, each carrying that day's refreshed score/comments/rank.

How do I avoid look-ahead bias with hn_stories?

Filter on hn_stories.available_date, the day the publisher made the row public. For this dataset that date is derived as follows — published: available_date = captured_date, the UTC calendar day the collector's snapshot run captured this row (STORED generated column, generated from captured_date itself since migration 000175 — previously synced_at::date, which is the same day on a live run but the REPLAY day on a bronze replay). A query of the form WHERE available_date <= '<as-of date>' never sees a row before it existed.

In what formats can I get hn_stories?

As a Parquet bulk export (POST https://api.obscura.trade/v1/download) or as JSON from the typed query API (POST https://api.obscura.trade/v1/query), both with dataset="hn_stories". The column schema is public at https://api.obscura.trade/v1/catalog/hn_stories.

How often is hn_stories updated?

Obscura refreshes hn_stories on a daily schedule — that is the most frequent scheduled job that re-collects or re-exports the table. It is Obscura's own pipeline cadence, not the upstream publisher's release schedule; when the publisher makes a row available is described by the availability rule above, and is recorded per row in available_date.

Related datasets

GDELT EventsGeopolitical and economic events extracted from worldwide news by the GDELT project, coded by actor, action and location… GitHub ReposActivity on tracked GitHub repositories: stars, forks and commit frequency. Google TrendsGoogle search interest over time for the full US equity universe by ticker, back two decades. Scores are 0-100 rescaled … Open Data CatalogDataset listings harvested from government open-data portals running CKAN. Web PopularityDaily website popularity rank of each public company's domain, from four independent top-sites lists (Majestic Million, … Wikipedia PageviewsDaily Wikipedia pageviews for a universe of public companies, by ticker. Wikipedia RevisionsEdit activity on tracked Wikipedia articles. Wikipedia TopWikipedia's most-viewed articles for each day.