In 14 years of wrestling with backends, I have seen plenty of “automated” systems that turn out to be a pile of fragile cron jobs held together by hope. A reliable data pipeline for crime trends cannot work that way. Dumping raw API responses straight into a database leaves you with a table nobody trusts, so you want a structured ETL (Extract, Transform, Load) workflow that expects failure and keeps going.
The pipeline below uses Python, PostgreSQL and Prefect, and it spends as much effort on validating data as on moving it. Same argument I made in my piece on why modern data stack consolidation matters: pick stability over shiny objects.
Designing the reliable data pipeline for crime trends
The Transform stage is where most developers come unstuck. They assume the source API, here the Socrata Open Data API, will always hand back clean records. I have watched pipelines die because a field that should have been a float showed up as a string. So the pipeline gets a validation layer of its own.
The stack for this implementation:
- Prefect runs the orchestration and the retries.
- PostgreSQL holds the data.
- Metabase handles visualization, in Docker.
- The Socrata API is the source of the local police log data.
Step 1: the extraction task
First, fetch the data from the Socrata Consumer API. Prefect’s task decorator takes care of retries, so when the API times out (and it will) the run backs off and tries again instead of dying.
@task(retries=3, retry_delay_seconds=[10, 30, 60])
def bbioon_extract_crime_data():
client = Socrata("data.cambridgema.gov", os.getenv("SOCRATA_TOKEN"), timeout=30)
results = client.get_all("3gki-wyrb")
return pd.DataFrame.from_records(results)
Step 2: validation and sanity checks
Validate the schema before anything reaches PostgreSQL, and expect the occasional non-numeric ID to sneak into the logs. The validation task raises a ValueError when the core schema does not look the way it should, which stops a bad batch at the door rather than three dashboards later.
@task
def bbioon_validate_schema(df):
REQUIRED_COLS = ['date_time', 'id', 'type', 'location']
for col in REQUIRED_COLS:
if df[col].isnull().any():
raise ValueError(f"CRITICAL: Missing data in {col}")
return df
Orchestration with Prefect and Docker
Running this script by hand works right up until the day you forget. A docker-compose.yml file brings up PostgreSQL and Metabase together, so the environment is reproducible on any machine. The Prefect documentation covers setting up workers inside containers.
The flow is what makes the reliable data pipeline for crime trends a pipeline rather than four scripts. The tasks get wired into one executable workflow on a cron schedule.
@flow(name="Crime_Data_ETL")
def bbioon_crime_flow():
raw_df = bbioon_extract_crime_data()
valid_df = bbioon_validate_schema(raw_df)
transformed_df = bbioon_transform_logic(valid_df)
bbioon_load_to_postgres(transformed_df)
if __name__ == "__main__":
bbioon_crime_flow.serve(name="daily-deployment", cron="0 0 * * *")
Visualizing trends in Metabase
With the data in PostgreSQL, connect Metabase via Docker. The Transform stage already deduplicates with drop_duplicates, so overlapping API fetches will not inflate the incident counts on the dashboard.
If this reliable data pipeline for crime trends work is eating your dev hours, I can take it off your plate. I have been wrestling with WordPress and messy backend integrations since the 4.x days.
Takeaway for senior developers
Automation is only as good as its error handling. A reliable data pipeline for crime trends takes more than a script on a timer. Prefect handles the orchestration and the retries, strict validation tasks catch the garbage early, and together they turn a fragile sync into something you can leave running. Bad data will still arrive; the win is that the pipeline says so out loud instead of quietly writing it to the table.