# CityMETER · Factory — data and method

How `data/factories.json` is built, what one row and one figure mean, and what the demo never claims. Every number below is
a snapshot of release 0.2.0 (data built 2026-11-05 from the research master checked 2026-11-04); the page computes
everything from the data at run time, so these are fixtures, not constants.

## วิธีนับ — อ่านตัวเลขในหน้าอย่างไร

หน้าจอไม่มีคำเตือน (กติกาเดียวกับ CityMETER: Hotel ที่เจ้าของกำหนด 1 พ.ย. 2569: “ไม่มี caveats เลย”) ทุกบรรทัดที่มาท้ายหน้ามีลิงก์ “วิธีนับ” มาที่นี่ ขอบเขตของตัวเลขอยู่ตรงนี้ที่เดียว

- **ขอบเขตชุดข้อมูล** = ทะเบียนโรงงานหลักทุกขนาดของชีตวิจัย (74,047 เลขทะเบียนโรงงาน 14 หลัก, FID): ทะเบียนในข้อมูลเปิดของกรมโรงงานอุตสาหกรรม จำพวก 2 และ 3 (71,126) ซึ่งครอบคลุมโรงงานนอกนิคมอุตสาหกรรมของ กนอ. และทะเบียนที่กระทรวงอุตสาหกรรมระบุดำเนินการแต่ไม่อยู่ในชุดของ กรอ. (2,921) — โรงงานในนิคมอุตสาหกรรมขึ้นทะเบียนกับ กนอ. และไม่อยู่ในชุดนี้ ยอดของอำเภอที่มีนิคมอุตสาหกรรมของ กนอ. จึงต่ำกว่าคนงานโรงงานทั้งหมดของอำเภอนั้น
- **คน** = จำนวนคนงานรวมที่ระบุในข้อมูลเปิดของ กรอ. (`คนงานรวม` ของไฟล์จำพวก 2 และ 3) รวมต่อพื้นที่ มีตัวเลข 71,124 ทะเบียน (391 ทะเบียนระบุ 0 คน ตามแหล่ง) อีก 2,923 ทะเบียนไม่มีตัวเลข (2 ทะเบียนช่องว่างในไฟล์ กรอ. และ 2,921 ทะเบียนที่มีเฉพาะใน อก.) — ไม่ถูกนับเป็น 0 และไม่ถูกประมาณแทน ข้อมูลเปิดไม่บอกวันที่ของตัวเลขนี้ จึงไม่ใช่การจ้างงานปัจจุบัน ตลาดแรงงาน หรือจำนวนคนที่อาศัยในพื้นที่
- **ไม่มีตัวเลขคนงาน** = ทะเบียนที่แหล่งไม่มีตัวเลขคนงาน แสดงเป็นคำนี้ หรือ “—” ในตาราง ในแผนที่ เขตหรือจังหวัดที่ทุกทะเบียนในตัวกรองไม่มีตัวเลขคนงานเป็นลายเส้นทแยง (ไม่มีข้อมูล) ไม่ใช่ 0
- **คนงาน 95 คนขึ้นไป** = เกณฑ์ P90 ของชีตวิจัย: เปอร์เซ็นไทล์ที่ 90 (PERCENTILE.INC 0.9) ของ 71,124 ตัวเลขคนงาน = 95 คน → 7,175 ทะเบียน (ตรงกับแท็บ `18_WORKERS_P90` ทุกทะเบียน รวมทะเบียนที่มีคนงานเท่าเกณฑ์พอดี 122 ทะเบียน) มีคนงานรวมกัน 2,435,798 คน หรือ 65% ของคนงานทั้งหมดในชุด เป็นตัวเลือกหนึ่งในตัวกรองขนาด
- **ทะเบียน (โรงงาน)** = หนึ่งเลขทะเบียน ไม่ใช่หนึ่งโรงงานจริงหรือหนึ่งบริษัท บริษัทหนึ่งมีได้หลายทะเบียน (ต่างโรงงาน ต่างกระบวนการ) และ 3,328 ทะเบียนมีชื่อและที่อยู่ตรงกับทะเบียนอื่น (`dup`) ซึ่งอาจเป็นไซต์เดียวกัน
- **สัดส่วนของจังหวัด** = คน (หรือทะเบียน) ในพื้นที่ ÷ ทั้งจังหวัดในตัวกรอง
- **อุตสาหกรรม** = กลุ่มอุตสาหกรรมทางการของ กรอ. ตามรหัสประเภทโรงงาน (`official_industry_group_raw`) — 21 กลุ่ม และ 3,114 ทะเบียนที่ไม่ระบุกลุ่ม — จัดเป็น 10 หมวดเพื่อใช้สีและรูปทรงบนแผนที่ (ตาราง `FAMILIES` ใน `scripts/build-data.py`) หมวดเป็นการจัดกลุ่มเพื่อการแสดงผล ชื่อกลุ่มของ กรอ. อยู่ในทุกแผงและกราฟ “กลุ่ม กรอ.”
- **หน้าที่ (ผลิต · บริการ · อื่น ๆ · รอตรวจหน้าที่)** = การจัดประเภทของชีตวิจัยจากประเภทโรงงานด้วยกฎ (functional-factory-classification v2, 4 พ.ย. 2569) ไม่ได้ตรวจทีละทะเบียนโดยคน
- **สถานะ อก.** = สถานะในข้อมูลที่ตั้งประกอบกิจการโรงงานของกระทรวงอุตสาหกรรม (GD Catalog) ณ วันที่ในแผง (“ไม่พบใน อก.” = ไม่พบเลขทะเบียนนี้ในข้อมูลชุดนั้น) ไม่ใช่การตรวจภาคสนามว่าโรงงานยังเปิดหรือปิด
- **จุด** = พิกัดจากแหล่งที่ชีตตรวจว่าอยู่ในกรอบประเทศไทย (`within_TH_bbox_unverified`) ไม่ใช่ตำแหน่งโรงงานที่ยืนยันแล้ว มี 15,654 ทะเบียน แหล่งไม่มีพิกัดสำหรับทะเบียนส่วนใหญ่ (55,772 ทะเบียน) จุดที่อยู่นอกอำเภอของทะเบียนเอง (718 ทะเบียน) ไม่แสดง (“ไม่แสดงพิกัด”) และทะเบียนอยู่ในอำเภอตามทะเบียนเสมอ ทุกทะเบียนนับในยอดของพื้นที่ แม้ไม่มีจุด
- **นิคม สวน เขตอุตสาหกรรม** = รายชื่อจากชีตวิจัยนิคมอุตสาหกรรม (106 รายการ ตรวจ 25–26 ต.ค. 2569) พร้อมลิงก์หน้าทางการ แสดงเป็นรายชื่อเท่านั้น ไม่มีขอบเขตบนแผนที่ (ชีตยังไม่เลือกขอบเขตและยังไม่ได้ตรวจสิทธิ์) และไม่ได้บอกว่าโรงงานใดอยู่ในนิคม
- **ผู้ประกอบการบุคคลธรรมดา** = ทะเบียนที่ชื่อเป็นชื่อบุคคล (6,036 ทะเบียน) แสดงโดยไม่มีชื่อและไม่มีจุด ยังนับในยอดของพื้นที่
- **โรงงานใกล้เคียง** = ทะเบียนหมวดอุตสาหกรรมเดียวกัน คนงานครึ่งเท่าถึงสองเท่า (ทุกขนาดเมื่อทะเบียนนั้นไม่มีตัวเลขคนงานหรือระบุ 0 คน) ในระยะตรง 3 กม. จากจุด เรียงใกล้ก่อน เป็นรายชื่อเริ่มต้นสำหรับเทียบ ไม่ใช่ห่วงโซ่อุปทานหรือคู่แข่งที่ยืนยันแล้ว
- **อาคาร** = อาคารบนแผนที่ (CityMETER Building V4 หรือ OpenStreetMap ตามพื้นที่) ความสูง “สูงราว” เป็นค่าประมาณ “โรงงานในอาคารนี้” = จุดของทะเบียนอยู่ในขอบอาคาร เมื่อจุดไม่อยู่ในอาคารใด มุมมองอาคารเปิดที่อาคารใกล้จุดที่สุด (≤ 300 ม. ไม่มีก็ ≤ 800 ม.) เป็นการเลือกมุมกล้องเท่านั้น
- **ภูมิประเทศ** = พื้นดินรอบจุด (รัศมี 500 ม.) จาก SRTM 30 ม. (สำรวจปี 2543) และแผนที่ OSM ไม่ใช่การประเมินความเสี่ยงน้ำท่วมหรือความเหมาะสมของที่ดิน

## 1. Sources

| Input | What it is | Published here? |
|---|---|---|
| `02_FACTORIES` of “CityMETER Factory Thailand — Factory, Operator & Company Research Master 2026-11-04” (Google Sheets, registry id `research.TH.factory`; columns listed in `scripts/build-data.py`) | the rows of this product: the 74,047 core FIDs with name, operator label, district, subdistrict, functional class, DIW class, type code, MOI status and its date, core basis, duplicate flag, estate context, source point and its screen | derived fields only (`data/factories.json`); the sheet and its export are never committed |
| `คนงานรวม` of the DIW class 2 and class 3 open-data exports (`factype2.csv`, `factype3.csv`, [diw-dataset.diw.go.th](https://diw-dataset.diw.go.th/th/dataset/factype3), resource updated 2026-10-01) | the worker figure of each DIW-listed FID; both files read 2026-11-05 with the SHA-256 the research master registers (`99_SOURCE_FILES`) | the figure only (fid · workers); every other field of the exports stays out |
| `18_WORKERS_P90` (gid 2026110418, same sheet) | the 7,175 FIDs at or above the P90 of the worker figures | a parity check: every figure equals the DIW export, and the tab is exactly the FIDs with 95+ workers |
| `02_TYPES` (same sheet, `02_FACTORIES` grouped by type code) | DIW type definition and official industry group per type code | code, definition and group |
| `99_SOURCE_FILES` (same sheet) | the source register: DIW class 2/3 exports (resource updated 2026-10-01), MOI location and permit exports (row updates through 2026-11-03), all checked 2026-11-04 | public https pages only |
| `01_ESTATES` of “CityMETER Industrial Estates — นิคม สวน และเขตอุตสาหกรรม 106 รายการ — Research Master 2026-10-25” | names, type, province and the official page of 106 industrial areas | the list only; no geometry is read |
| `data/area-index.json`, `data/frames/`, `data/provinces.geojson`, `data/boundary-receipts.json` | CityMETER production district and province outlines and base info (read 2026-11-01), copied from `montri-th/hotel@177e592` | yes (display geometry, production codes) |
| `data/building-units/` | buildings per อปท./เขต by the 15 % rule, from `montri-th/hotel@177e592` (units that hold a hotel with a point); every other unit draws OpenStreetMap | yes |

SHA-256 of every input file read is in `meta.inputs` (the files themselves stay in the owner's research workspace).

## 2. One row, two measures

One row = one FID of `02_FACTORIES`. Its worker figure is `คนงานรวม` of its DIW export row; the build stops if a FID of the
exports is missing from the registry, if a DIW-listed FID has no export row, if a figure differs from `18_WORKERS_P90`, if the
P90 of the figures is not 95, or if `18_WORKERS_P90` is not exactly the FIDs with 95+ workers. Nothing is merged, split, moved
or re-labelled. The page ranks the registrations by workers and builds the short label names itself (`js/model.js`).

| Measure | Unit | Definition | LDS family |
|---|---|---|---|
| `workers` (default) | คน | Σ `workers` of the registrations with a figure (3,755,632 in all; median 15, max 13,200) | `count` (a total, no denominator) |
| `count` | ทะเบียน | number of FIDs | `count` |

Both measures are totals: no per-km² or per-resident ratio is drawn, so no density family applies. A registration without a
figure (`workers = null`, 2,923) adds nothing to a worker total and is never estimated; where every registration of a district
or province in the selection lacks a figure, its worker total is EVID-05 no data (the hatch), never a measured 0.

Worker bands (ทะเบียน): 0–9 คน 23,462 · 10–49 33,178 · 50–94 7,309 · 95–199 3,998 · 200–499 2,216 · 500–999 585 ·
1,000–1,999 244 · 2,000–4,999 108 · 5,000+ 24 · ไม่มีตัวเลขคนงาน 2,923. The size filter offers “คนงาน 95 คนขึ้นไป” (`p90`,
7,175), every band and “ไม่มีตัวเลขคนงาน” (`none`); the band ids `w1`…`w6` keep their release 0.1.0 meaning.

## 3. District membership (build time, never re-assigned on screen)

The registration's own district label (`02_FACTORIES.district`, DIW 2026 first, otherwise MOI) is matched by name to a
production district of its own province (`row.admin = name`, 74,045 rows). A label that names no district of its province
leaves the row at province level (`province`, 2 rows). The source point never decides membership.

## 4. Points

`latitude_observed` / `longitude_observed` are published only where `coordinate_quality` is `within_TH_bbox_unverified`
(17,392 registrations; the sources have no point for 55,772 and an unusable one for 883), rounded to 5 decimals, and only
when the point falls inside the registration's own district outline: 585 points in another district, 128 in another province
and 5 outside every outline are withheld (`row.off`, `meta.checks.pointWithheld`). Points of registrations known only by a
natural person's name (1,020 of them had one) are not published. 15,654 rows carry a point; the others count in their
district and are listed in panels and the table without a symbol.

## 5. Industry, function and status

- **Type → group → family:** `factory_type_code_raw` → `official_industry_group_raw` (one group per code) → one of ten
  families (`FAMILIES`): food · electrical · vehicle (vehicles and machinery, which in the DIW grouping includes electronic
  computing devices such as type 06900) · textile · metal · polymer · wood · mineral · plant · other (incl. the 3,114 rows
  with no group). The family binds to an LDS categorical series and its shape cue by id (`js/cues.js#GROUP_SERIES`).
- **Function:** `functional_class` of `02_FACTORIES` (manufacturing 54,229 · review 7,729 · other 6,967 · service 5,122).
  All classes stay in the legal-factory scope.
- **DIW class (จำพวก):** 3 for 67,778 rows, 2 for 3,348; the 2,921 MOI-only registrations have no DIW class.
- **MOI status:** `moi_location_status_raw` (ดำเนินการ 27,007 · ได้รับใบอนุญาต 3,621 · หยุดดำเนินการ 902 · จำหน่าย 9 ·
  not found 42,508) with its update date. A stopped or removed status is not read as a closure of the site.
- **Core basis:** listed in the DIW export of 2026-10-01 (47,040), listed and MOI-operating (24,086), MOI-operating only
  (2,921).

## 6. Context (build time)

- **Industrial areas:** 106 names in 21 provinces (84 นิคมอุตสาหกรรม · 16 สวนอุตสาหกรรม · 6 เขตอุตสาหกรรม), each with its
  official page (IEAT estate pages, developers' pages, government pages). The list never says which registration lies in an
  estate: the DIW-listed rows have `estate_context = outside_estate_source_scope_DIW_catalog`, the MOI-only rows
  `unknown_estate_context`, and every row `estate_verification_status = individual_membership_unverified`.
- The panels read the 95+ lens and the functional classes from the same rows; release 0.1.0's separate registry context is
  gone because the rows are now the registry.

## 7. What the page computes

- **First Aha** (`js/area-insight.js`): the province holding the most of the measure among rows placed at district level;
  up to three of its districts with the most of it (never cutting a tie; never every district that holds the measure); their
  share of the province's whole total; the industry families of those rows with each share (numerator and denominator in
  the title and accessible text); the district with the most of the measure outside that province and its first family.
  Snapshot: 72 % of the workers of Samut Prakan (330,987 of 457,353) are in Bang Phli, Mueang Samut Prakan and Phra Pradaeng;
  with the 95+ lens the same three อำเภอ hold 74 % (220,980 of 299,796), as in release 0.1.0.
- **Ranks and charts:** districts ranked over `scopeRows` (filters before any area selection); the matrix of district ×
  family with equal cells; the official DIW groups ranked by the measure.
- **Nearby set** (`js/profile-model.js#nearbyOf`): same family, workers ½–2× (any size without a figure), straight line
  ≤ 3 km, every row of the dataset, nearest first; the count and workers of every registration within 3 km beside it.
- **Map colour:** the LDS `count` family, all 41 samples by rank among the measured nonzero values of the districts being read
  (`js/scales.js#buildRamp`); a district with no registration is a measured zero (sample 0 + the zero outline): 13 of 928
  districts unfiltered (12 without a registration, 1 whose registrations record 0 workers). Workers unknown for every
  registration of an area under the filters → no data (the 135° hatch), for districts and provinces alike.

## 8. Not published

Addresses, raw MOI operator names (ONAME), the 2025 operator label, operator context ids, legal operator and parent fields,
researcher notes and human review fields, evidence and conflict flags, source record ids, logo files and URLs, points outside
the screen or outside the registration's own district, names and points of natural-person operators, private Drive or Sheet
URLs, and every field of the DIW exports except the FID and `คนงานรวม` (`meta.excludedFields`). Estate boundaries are not
published. Operator labels (`operator_display_observed`) are shown only where they differ from the registration's name
beyond spacing and legal-form words (3,405 rows).

## 9. Research follow-ups (reported, not patched in `data/`)

1. **IEAT estates:** factories registered with IEAT (the sheet's IEAT portal tab holds 6,937 source records) are outside this
   product; a dated worker figure per IEAT factory would be needed to compare estate districts fairly.
2. **Worker date:** the DIW exports carry no date for the worker figure; a dated source would let the panel say “ณ ปี …”.
3. **Same-site registrations:** 3,328 rows share an exact name and address with another FID; a site key would let the map
   show one plant once.
4. **Company logos:** the sheet selects logos for 457 FIDs (`20_LOGO_USE`, supported company context); the files are in the
   research workspace and are not in this release.
5. **Estate boundaries:** `19_MAP_EXPORT` of the estate master selects no boundary yet; rights are not cleared.
6. **Building units:** the 15 % rule was decided for the units that hold a hotel; units holding factories only draw OpenStreetMap
   until `scripts/build-building-units.py` is run for them.
7. **Points:** the sources have no point for 75 % of the registrations; a geocoded point per FID (with its method) would put
   them on the map.

## 10. Rebuild

```sh
python3 -I scripts/build-data.py --csv <dir of tab CSV exports> --workers <fid,workers,class of the DIW exports> \
  --estates <dir of estate-master CSVs> --index data/area-index.json --frames data/frames \
  --out data/factories.json --report data/build-report.json --built-on YYYY-MM-DD
npm test
```

The CSV exports are read from the research master (tabs listed in §1) and the DIW open-data portal, and stay outside the
repository. Changes to names, links, statuses or classes are made in the research sheet itself, then re-exported and rebuilt —
never patched in `data/`.
