monitorulpreturilor.gov2.ro – interfață alternativă pentru monitorulpreturilor.info
Preia și stochează date despre prețurile alimentelor și ale combustibililor de pe Monitorului Prețurilor, proiect al Consiliului Concurenței
Proiectul Monitorul Prețurilor produselor alimentare își propune să acorde consumatorilor posibilitatea de a compara prețul aferent coșului de produse a cărui achiziție intenționează să o realizeze.
Fetch (writes to DB):
fetch_reference.py # once / weekly — retail reference data
└─ fetch_prices.py # daily — retail prices (per-store, population-ordered)
└─ build_price_flags.py # daily — price quality flags (outlier, spike, promo)
fetch_gas_reference.py # once / weekly — gas reference data
└─ fetch_gas_prices.py # daily — gas prices
discover_stores.py # one-shot — store discovery from locality centroids
└─ update_store_populations.py # after discovery — enrich stores with population
Build (DB → site/data/, run after fetch, before site generation):
build_baskets.py # basket costs per network + UAT → site/data/baskets/
build_anomalies.py # price-anomaly feed → site/data/anomalies_today.json
build_categories.py # per-category spreads → site/data/categories/
build_cpi.py # basket cost trend → site/data/cpi.json
build_stores_index.py # store geolocation index → site/data/stores_index.json (needs baskets)
build_uat_geojson.py # UAT choropleth data → site/data/uats.geojson (needs baskets)
Site generation (site/data/ → site/*.html):
generate_site.py # static site (9 HTML pages + per-product CSVs)
generate_pipeline_report.py # pipeline health page → site/pipeline-health.html
generate_map.py # standalone Leaflet store map → dashboard/stores_map.html
export_analytics.py # export DB views to site/data/*.csv
Analysis (standalone, read-only):
analyse_prices.py # price variability: intra-network + cross-network CSVs
analyse_products.py # brand/word frequency + category anomaly CSVs
analyze_prices.py # price uniformity: % of uniform-price groups per network
CI helpers:
build_ci_subset.py # build store + product ID subsets for GitHub Actions
# 1. Retail reference data (networks, UATs, categories, products)
python fetch_reference.py
# 2. Discover all stores across Romania
python discover_stores.py
# 3. Enrich stores with surrounding population (used for fetch ordering)
python update_store_populations.py
# 4. Gas reference data (networks, fuel types)
python fetch_gas_reference.pyCron layout (VPS): retail (
fetch_prices.py) runs every 30 minutes (*/30 * * * *) with--max-runtime 1700(28 min 20 s), so each slice stays within its window and the lock/checkpoint carry state across firings. Gas (fetch_gas_prices.py) runs independently at 03:00. They are not chained — a stalled retail run does not block the gas fetch.Monitoring: each cron line is wrapped with
scripts/hc_run.sh <uuid> <cmd>, which pings healthchecks.io with/start, then either the base URL on success or/failon non-zero exit. Each fetcher's line also runscheck_runs.pyafterwards — so a fetch that "completed" but wrote zero records still trips/fail. A dailyaudit_pipeline.pyat 06:00 checks data quality (store freshness, abandoned runs, network coverage gaps) and fails the same way. Seescripts/crontab.templatefor the canonical layout, anddocs/monitoring-pattern.mdfor the reusable approach.Logs: all cron output now writes to
data/logs/(created on demand, gitignored). Historical logs in~/g2-dev/logs/are left in place; new writes go to the project-local path.
# Fetch
python fetch_prices.py # retail — resumes from checkpoint if interrupted
python fetch_gas_prices.py # gas — resumes from checkpoint if interrupted
# Build (order matters: baskets first, then the rest)
python build_price_flags.py # price quality flags → DB
python build_baskets.py # basket costs → site/data/baskets/
python build_anomalies.py # price anomalies → site/data/anomalies_today.json
python build_categories.py # category spreads → site/data/categories/
python build_cpi.py # basket cost trend → site/data/cpi.json
python build_stores_index.py # store index → site/data/stores_index.json
python build_uat_geojson.py # UAT map data → site/data/uats.geojson
# Generate site
python export_analytics.py # CSV snapshots → site/data/
python generate_site.py # HTML pages → site/
python generate_pipeline_report.py # pipeline health → site/pipeline-health.htmlpython fetch_reference.py
python fetch_gas_reference.py
python discover_stores.py # picks up newly opened stores
python update_store_populations.pypython analyse_prices.py # → data/price_intra_network.csv, data/price_cross_network.csv
python analyse_products.py # → data/brands.csv, data/product_words.csv, data/category_anomalies.csv| File | Created by | Purpose |
|---|---|---|
prices.db |
db.py (via any script) |
Main SQLite database — all tables |
prices.db-wal |
SQLite (WAL mode) | Write-ahead log; normal when a script is running or was interrupted. Checkpointed automatically on clean close. |
prices.db-shm |
SQLite (WAL mode) | Shared-memory index for the WAL; accompanies prices.db-wal |
prices_checkpoint.json |
fetch_prices.py |
Retail fetch progress; deleted/reset on fresh run |
gas_checkpoint.json |
fetch_gas_prices.py |
Gas fetch progress; deleted/reset on fresh run |
discover_stores_checkpoint.json |
discover_stores.py |
Store discovery progress; deleted/reset on fresh run |
retail_checkpoint.json |
fetch_prices_by_uat.py (legacy) |
Legacy UAT-based fetch progress |
price_intra_network.csv |
analyse_prices.py |
Intra-network price variance |
price_cross_network.csv |
analyse_prices.py |
Cross-network price comparison |
brands.csv |
analyse_products.py |
Normalized brand list |
product_words.csv |
analyse_products.py |
Word/bigram frequency in product names |
category_anomalies.csv |
analyse_products.py |
Products assigned to unexpected categories |
discover_gas_checkpoint.json |
discover_gas_stations.py |
Gas station discovery progress; deleted/reset on fresh run |
ci_stores.txt |
build_ci_subset.py |
Store IDs for CI test runs (one per line) |
ci_products.txt |
build_ci_subset.py |
Product IDs for CI test runs (one per line) |
WAL note:
prices.db-walandprices.db-shmare safe to leave in place between runs — SQLite manages them. Only delete them if you're sure no script is writing (e.g., after a crash and before restoring from backup).
Fetches slow-changing reference data: retail networks, UATs, product categories, and products. Run once, or weekly to pick up new products.
python fetch_reference.py # full run → data/prices.db
python fetch_reference.py --limit 5 # first 5 categories only (for testing)
python fetch_reference.py path/to/db.db # custom DB pathFetches current prices for all stores × product combinations, ordered by store population (busiest cities first). Saves progress to data/prices_checkpoint.json so interrupted runs resume automatically. Requires reference data — run fetch_reference.py first.
Checkpoint behaviour:
- Interrupted run → resumes from last saved position on next run
- Completed run, same day → exits immediately (no redundant API calls)
- Completed run, new day → starts a fresh run automatically
--fresh→ ignores any checkpoint and starts clean--resume→ continues a completed same-day run; skips already-processed store×batch keys, fetches only new stores
Sweep duration: with ~87 K products (437 batches/anchor at BATCH_SIZE=200) and 683 anchors, one full pass takes roughly 7–9 daily cron windows (23 h each). The checkpoint resumes across days using the same
fetched_attimestamp, so all prices within a single logical sweep are consistent.price_datereflects the API-reported store date, not the fetch date.
Run history: every invocation inserts a row into the
runstable. Rows left asstatus='running'by a prior crash or SIGKILL are automatically marked'abandoned'at the next startup.
Product ordering (--products-order):
db(default) — products in DB insertion orderstale— never-fetched products first, then sorted by oldestfetched_atascending. Use with--max-runtimeto cap daily overhead while always filling coverage gaps first. On interrupted runs the product order is saved in the checkpoint so resume is stable within the same day; the next day's run re-derives staleness from the DB.
python fetch_prices.py # full run → data/prices.db
python fetch_prices.py --resume # fetch only new stores added since today's run
python fetch_prices.py --products-order stale --max-runtime 3600 # prioritise never-fetched / stale products, stop after 1h
python fetch_prices.py --limit-stores 3 --limit-products 90 # quick smoke test
python fetch_prices.py --fresh # ignore checkpoint, start clean
python fetch_prices.py path/to/db.db # custom DB pathDiscovers retail stores by probing the API from Romanian locality centroids (3 180 localities from the official population CSV). Deduplicates probe points within 4 km to avoid redundant calls. Writes stores to DB; does not write prices. Resumes from checkpoint on restart.
Run once (or after a long gap) to populate the stores table before running fetch_prices.py.
python discover_stores.py # all localities ≥ 2 500 pop
python discover_stores.py --min-pop 5000 # larger threshold
python discover_stores.py --limit 50 # first 50 probe points (quick test)
python discover_stores.py --dry-run # parse + log, no DB writes
python discover_stores.py --fresh # ignore checkpoint, restart
python discover_stores.py --debug # verbose loggingComputes a surrounding_population estimate for every store: sums populations of all localities whose centroid falls within --radius km (default 10 km). Used by fetch_prices.py to prioritise high-traffic stores.
Run once after discover_stores.py, or re-run whenever the stores table grows.
python update_store_populations.py # default 10 km radius
python update_store_populations.py --radius 5
python update_store_populations.py --debugEarlier store-discovery approach using UAT centroids from the DB (only ~20 UATs loaded by default). Superseded by discover_stores.py which covers all 3 180 Romanian localities. Kept for reference.
python discover_stores_by_uat.py [--pop-threshold 10000] [--debug] [--dry-run]Earlier price-fetching approach: iterates UAT × product batches using the centroid endpoint. Superseded by fetch_prices.py which fetches per-store with population ordering. Kept for reference.
python fetch_prices_by_uat.py
python fetch_prices_by_uat.py --limit-uats 3 --limit-products 90
python fetch_prices_by_uat.py --freshFetches gas networks and fuel product types (6 fuel types: benzină/motorină standard & premium, GPL, electric). Run once or weekly.
python fetch_gas_reference.py # full run → data/prices.db
python fetch_gas_reference.py path/to/db # custom DB pathFetches current fuel prices for all UATs (one request per fuel type per UAT). Saves progress to data/gas_checkpoint.json. Same checkpoint behaviour as the retail pipeline.
python fetch_gas_prices.py # full run → data/prices.db
python fetch_gas_prices.py --limit-uats 3 # quick smoke test
python fetch_gas_prices.py --fresh # ignore checkpoint, start clean
python fetch_gas_prices.py path/to/db # custom DB pathDiscovers gas stations by probing GetGasItemsByLatLon from populated locality centroids (≥ 2 500 pop by default). Uses Motorină standard (fuel type 21) to find all stations within 5 km. Resumes from data/discover_gas_checkpoint.json.
Run once (or periodically) before fetch_gas_prices.py to populate the gas_stations table.
python discover_gas_stations.py # all localities ≥ 2 500 pop
python discover_gas_stations.py --min-pop 5000 # larger threshold
python discover_gas_stations.py --limit 50 # first 50 probe points (quick test)
python discover_gas_stations.py --dry-run # no API calls or DB writes
python discover_gas_stations.py --fresh # ignore checkpoint
python discover_gas_stations.py --debug # verbose loggingAnalyses retail price variability. Produces two CSVs in data/:
| Output | Contents |
|---|---|
price_intra_network.csv |
Per (product, network): min/max/avg/CV/store count — flags chains with non-uniform pricing |
price_cross_network.csv |
Per product: cheapest/most-expensive network, spread, ratio, price per network |
Unit normalization collapses kg/Kg/1kg etc. to a common bucket. SELGROS (B2B wholesale) is excluded from cross-network rankings by default.
python analyse_prices.py
python analyse_prices.py --min-stores 3 # require at least 3 stores per group
python analyse_prices.py --include-selgros # include SELGROS in rankings
python analyse_prices.py --debugAnalyses price uniformity: for each (product, network, date) group, counts distinct prices across stores. Reports what percentage of groups are uniformly priced and which products have the highest within-network variance. Outputs site/price_uniformity.csv.
python analyze_prices.pyAnalyses product names and brands. Produces three CSVs in data/:
| Output | Contents |
|---|---|
brands.csv |
Normalized brands with raw variants, counts, parent categories |
product_words.csv |
Most common words and bigrams in product names (diacritic-insensitive) |
category_anomalies.csv |
Products whose brand dominates one category but the product is assigned elsewhere |
python analyse_products.py
python analyse_products.py --db path/to/prices.db --top 200
python analyse_products.py --anomaly-threshold 0.85These scripts transform the raw DB data into JSON/CSV files consumed by the site. Run them after the daily fetch and before site generation. build_baskets.py must run first — other scripts read its output.
Persists price quality flags to the price_flags table in the DB. Run after each daily fetch. Safe to re-run (uses INSERT OR IGNORE). Flags: outlier_price (median + MAD), price_spike (day-over-day jump), promo_too_deep (>90% discount).
python build_price_flags.py
python build_price_flags.py --db path/to/prices.dbScores each curated basket (from config/baskets.json) at every retail network nationally and per UAT — picking the cheapest substitute SKU for each item. Outputs basket cost data used by the Coșul de Cămară page, and as input to build_stores_index.py and build_uat_geojson.py.
python build_baskets.py # → site/data/baskets/
python build_baskets.py --out path/to/dir
python build_baskets.py --db path/to/dbFor the latest price snapshot, finds products whose priciest-network price is ≥1.5× their cheapest-network price. Drives the Anomalii page ("save X lei by buying at Y instead of Z"). Excludes B2B networks and applies the same outlier filter as build_baskets.py.
python build_anomalies.py # → site/data/anomalies_today.json
python build_anomalies.py --out path/to/fileFor the latest price snapshot, ranks products within each category by cross-network price ratio. Drives the Category Explorer page.
python build_categories.py # → site/data/categories/
python build_categories.py --out path/to/dirTracks the national cheapest-network basket cost for each curated basket across all available price dates. Drives the price trend chart. Becomes more meaningful as historical data accumulates.
python build_cpi.py # → site/data/cpi.json
python build_cpi.py --out path/to/fileEmits a compact store list with coordinates, network, and UAT basket cost for each store — used by the Aproape de tine geolocation page. Requires build_baskets.py output.
python build_stores_index.py # → site/data/stores_index.json
python build_stores_index.py --out path/to/fileDecodes the Romania UAT TopoJSON (config/geo/ro-uats.topojson) and joins it with DB store counts and basket costs per UAT. Drives the choropleth map. Keeps only UATs that have at least one retail store. Requires build_baskets.py output.
python build_uat_geojson.py # → site/data/uats.geojson
python build_uat_geojson.py --out path/to/fileGenerates the full static site from the database — 9 HTML pages (dashboard, price index, fuel leaderboard, pipeline health, store map, trends, compare, analytics, gas map) plus per-product CSVs in site/data/products/.
python generate_site.py # → site/
python generate_site.py --out path/to/out # custom output directory
python generate_site.py --db path/to/db # custom DB pathGenerates a self-contained HTML pipeline diagnostic report with traffic-light indicators for store freshness, run completion, price outliers, price change velocity, and promo sanity.
python generate_pipeline_report.py # → site/pipeline-health.html
python generate_pipeline_report.py --out path/to/file
python generate_pipeline_report.py --db path/to/prices.dbOne-shot CLI digest of pipeline state — three sections: last N runs (default 10), per-script ok/fail summary over the last 7 days, and the latest data-quality audit verdict (read from data/logs/audit-*.json, never recomputed). Read-only, stdlib only, always exits 0. ANSI colours auto-disable when piping or with --no-color.
python status.py # last 10 runs + 7d summary + latest audit
python status.py --runs 20 # show last 20 runs
python status.py --days 14 # 14-day per-script summary
python status.py --no-color # plain textFast post-fetch verification — used in the cron wrapper to convert a "fetch exited 0" into a real health signal. Queries the runs table for the most recent status='completed' row matching --script; fails if it's stale, has zero records_written, or is missing. Honours a per-script lock file (e.g. data/prices_fetch.lock) so long resumes don't trip a false fail.
python check_runs.py --script fetch_prices --max-age-hours 25
python check_runs.py --script fetch_gas_prices --max-age-hours 25Daily data-quality audit. Reuses signal loaders from generate_pipeline_report.py. Writes both a text trail (data/logs/audit-YYYY-MM-DD.txt) and a JSON summary for later aggregation. Exits non-zero if any RED threshold is breached (store freshness >10% stale, any abandoned/error run in the last 7d, any retail network with no fresh prices in 7d, today's price_flags count >3× the 30-day median). Uses a read-only connection so it never blocks the live fetcher.
python audit_pipeline.py # → data/logs/audit-*.{txt,json}
python audit_pipeline.py --include-outliers # also run the slow per-product outlier checkBash wrapper that pings healthchecks.io /start before a command and either the base URL on success or /fail on non-zero exit. Set HC_RUN_DRYRUN=1 to skip curl calls for local testing.
scripts/hc_run.sh <uuid> python fetch_prices.py
HC_RUN_DRYRUN=1 scripts/hc_run.sh test-uuid bash -c 'echo ok'Generates a self-contained Leaflet interactive map of retail stores with network colour coding and marker clustering. Standalone alternative to the map page built by generate_site.py.
python generate_map.py # → dashboard/stores_map.html
python generate_map.py --out site/stores_map.html
python generate_map.py --db path/to/prices.dbExports 8 analytical SQL views to CSV files in site/data/ for use by the static site and external tools.
| Output CSV | Contents |
|---|---|
price_variability.csv |
Per-product price variability across networks |
cross_network_spread.csv |
Cross-network price spread per product |
popular_products.csv |
Top 200 most-recorded products |
private_labels.csv |
Top 100 private-label candidates |
stores_per_network.csv |
Store count per retail network |
price_freshness.csv |
Price freshness for the last 30 days |
products_no_prices.csv |
Products with no price records |
run_history.csv |
Last 30 pipeline run records |
python export_analytics.py # → site/data/
python export_analytics.py --out path/to/ # custom output directory
python export_analytics.py path/to/db # custom DB pathBuilds compact store and product ID lists for GitHub Actions CI, selecting representative stores by network rank × population geography and products by coverage rank. Outputs two text files used to limit CI fetch scope.
python build_ci_subset.py # defaults: top-10 per net, top-50 products
python build_ci_subset.py --top-per-net 5 --top-overall 30
python build_ci_subset.py --debug # print selection details
python build_ci_subset.py path/to/db # custom DB pathOutputs: data/ci_stores.txt, data/ci_products.txt (one ID per line).
| File | Role |
|---|---|
db.py |
init_db(path) creates all tables; upsert/insert helpers for retail and gas |
api.py |
fetch_xml(url) with retry/backoff; all XML parsers; centroid_from_wkt(wkt) |
networks.py |
Short display names (LIDL, Carrefour, …) + is_b2b() flag for all retail and gas networks |
units.py |
Normalizes the raw prices.unit column to canonical buckets (kg, l, buc) across the wildly inconsistent per-network formats |
Not run directly.
Dev tool that probes for undocumented API endpoints via WSDL/MEX metadata, root-level service discovery, and candidate pattern matching. Outputs findings to the terminal and docs/reference/undocumented-endpoints.md.
python explore_api.pyOne-time migration: populates the prices_current table from the existing prices table by taking the most recent price per (product_id, store_id). Safe to re-run (uses upsert). Run once after the table was introduced.
python backfill_prices_current.py
python backfill_prices_current.py path/to/prices.dbAll data is stored in data/prices.db (SQLite).
retail_networks (id TEXT PK, name, logo_url)
uats (id INT PK, name, route_id, wkt, center_lat, center_lon)
categories (id INT PK, name, parent_id, logo_url, source TEXT)
products (id INT PK, name, categ_id)
stores (id INT PK, name, addr, lat, lon, uat_id, network_id, zipcode,
surrounding_population)
prices (id AUTOINCREMENT PK, product_id, store_id, price, price_date, promo,
brand, unit, retail_categ_id, retail_categ_name,
fetched_at, last_checked_at,
UNIQUE(product_id, store_id, price_date))gas_networks (id TEXT PK, name, logo_url)
gas_products (id INTEGER PK, name, logo_url) -- 6 fuel types
gas_stations (id INTEGER PK, name, addr, lat, lon, uat_id, network_id, zipcode, update_date)
gas_prices (id AUTOINCREMENT PK, product_id, station_id, price, price_date,
fetched_at, last_checked_at,
UNIQUE(product_id, station_id, price_date))Reference tables use INSERT OR REPLACE; price tables use INSERT OR IGNORE.
runs (id AUTOINCREMENT PK, script TEXT, started_at TEXT, finished_at TEXT,
status TEXT, -- 'running' | 'completed' | 'interrupted' | 'abandoned' | 'error'
uats_processed INT, records_written INT, notes TEXT)Each invocation of fetch_prices.py or fetch_gas_prices.py creates one row. Status 'abandoned' means the process was killed (SIGKILL / unattended-upgrades) before it could write a final status — the checkpoint is still valid and will resume correctly on the next run.
# latest retail prices
sqlite3 data/prices.db "SELECT s.name, p.price, p.price_date FROM prices p JOIN stores s ON p.store_id=s.id ORDER BY p.price_date DESC LIMIT 20;"
# latest gas prices
sqlite3 data/prices.db "SELECT n.name, pr.name, gp.price FROM gas_prices gp JOIN gas_stations s ON gp.station_id=s.id JOIN gas_networks n ON s.network_id=n.id JOIN gas_products pr ON gp.product_id=pr.id LIMIT 20;"
# store count per network
sqlite3 data/prices.db "SELECT n.name, COUNT(*) FROM stores s JOIN retail_networks n ON s.network_id=n.id GROUP BY n.name ORDER BY 2 DESC;"See docs/queries.md for more.
Both APIs return XML with no authentication required. They share the XML namespace: http://schemas.datacontract.org/2004/07/pmonsvc.Models.Protos
| Endpoint | Description |
|---|---|
GET /GetRetailNetworks |
All retail chains (Kaufland, Lidl, etc.) |
GET /GetUATByName |
Top UATs; add ?uatname= to search |
GET /GetProductCategoriesNetwork |
Product category tree |
GET /GetProductCategoriesNetworkOUG |
OUG (emergency ordinance) categories |
GET /GetCatalogProductsByNameNetwork?prodname= |
Search products by name |
GET /GetCatalogProductsByNameNetwork?CSVcategids= |
Products by category ID(s) |
GET /GetCatalogProductsById?csvcatprodids= |
Products by ID(s) |
GET /GetStoresForProductsByLatLon?lat=&lon=&buffer=&csvprodids=&OrderBy=price |
Stores + prices near a coordinate (max buffer ~5 000 m, max 50 stores) |
GET /GetStoresForProductsByUat?uatId={}&csvprodids=[]&csvnetworkids=[]&OrderBy=price |
Stores + prices for a UAT |
Sample responses: docs/reference/sampleResponses/
| Endpoint | Description |
|---|---|
GET /GetGasNetworks |
All fuel networks (Petrom, OMV, MOL, Rompetrol, etc.) |
GET /GetGasProductsFromCatalog |
Fuel types (benzină/motorină standard & premium, GPL, electric) |
GET /GetGasServicesFromCatalog |
Station services catalog (shop, ATM, car wash, etc.) |
GET /GetUATByName |
Same UAT search as retail |
GET /GetGasItemsByUat?UatId=&CSVGasCatalogProductIds=&OrderBy=dist |
Stations + prices for a UAT (one product ID per request) |
GET /GetGasItemsByRoute?startRoutePointId=&endRoutePointId=&CSVGasCatalogProductIds=&OrderBy=dist |
Stations + prices along a route |
Sample responses: docs/carburanti/reference/
Note: GetGasItemsByUat returns HTTP 500 (not empty) for UATs with no stations — handle gracefully.
- Figure out API (retail + gas)
- Create fetching scripts (retail + gas)
- Store to DB
- Resume interrupted runs (checkpoint files)
- Track last-checked time per price (
last_checked_at) - Store discovery from full locality list (not just top UATs)
- Population-weighted store ordering in
fetch_prices.py - Price variability analysis (
analyse_prices.py) - Brand/product word analysis (
analyse_products.py) - Do carburanți
- Automated daily fetching (cron / scheduler)
- UI — monitor price variations over time
- Check price differences per UAT — maybe skip low-value UATs?
- Make list of relevant products and fetch those more often
- Cross-reference with https://ro.openfoodfacts.org/ / suntfrugal
- Deduplicate products with different names but same item
- Remove dedicated brands — map to existing known brands
- check stores/location from official sites, osm?
- alternative/own API?
Vezi și: backlog
- Does same network has different prices for different stores? — document scope and frequency