<?xml version="1.0" encoding="utf-8"?>

<feed xmlns="http://www.w3.org/2005/Atom" >
  <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator>
  <link href="https://notes.labordata.bunkum.us/feed.xml" rel="self" type="application/atom+xml" />
  <link href="https://notes.labordata.bunkum.us/" rel="alternate" type="text/html" />
  <updated>2026-07-18T09:15:49+00:00</updated>
  <id>https://notes.labordata.bunkum.us/feed.xml</id>

  
  
  

  

  

  
    <author>
        <name>Forest Gregg</name>
      
      
        <uri>https://bunkum.us/</uri>
      
    </author>
  

  
  
  
  
  
  
    <entry>
      

      <title type="html">DuckDB backend for labordata</title>
      <link href="https://notes.labordata.bunkum.us/2026/07/16/duckdb-backend" rel="alternate" type="text/html" title="DuckDB backend for labordata" />
      <published>2026-07-16T00:00:00+00:00</published>
      <updated>2026-07-16T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2026/07/16/duckdb-backend</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2026/07/16/duckdb-backend"><![CDATA[<p>Last month, I switched the backend database for labordata from
<a href="https://en.wikipedia.org/wiki/SQLite">SQLite</a> to
<a href="https://en.wikipedia.org/wiki/DuckDB">DuckDB</a> for clearer and faster queries.</p>

<p>Labordata should be easier to use and much snappier. Unfortunately, some older
saved queries may not work because of changes between the SQL dialect. Please
get in touch if you need help or have run into any other problems.</p>

<h2 id="dialect-improvements">Dialect Improvements</h2>

<p>SQLite’s minimalist SQL dialect and loose typing can lead to complicated
queries.</p>

<p>Here are two queries that both get the upper and lower bound on count of LM20
contracts filed per year. The first is in the SQLite dialect:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">most_recent_amendment</span> <span class="k">as</span> <span class="p">(</span>
  <span class="k">select</span>
    <span class="o">*</span>
  <span class="k">from</span>
    <span class="p">(</span>
      <span class="k">select</span>
        <span class="o">*</span>
      <span class="k">from</span>
        <span class="n">filing</span>
      <span class="k">order</span> <span class="k">by</span>
        <span class="n">srNum</span><span class="p">,</span>
        <span class="n">beginDate</span><span class="p">,</span>
        <span class="n">endDate</span><span class="p">,</span>
        <span class="n">formFiled</span><span class="p">,</span>
        <span class="n">yrCovered</span><span class="p">,</span>
        <span class="n">amendment</span> <span class="k">desc</span>
    <span class="p">)</span>
  <span class="k">group</span> <span class="k">by</span>
    <span class="n">srNum</span><span class="p">,</span>
    <span class="n">beginDate</span><span class="p">,</span>
    <span class="n">endDate</span><span class="p">,</span>
    <span class="n">formFiled</span><span class="p">,</span>
    <span class="n">yrCovered</span>
<span class="p">),</span>
<span class="n">new_contracts_lower</span> <span class="k">as</span> <span class="p">(</span>
  <span class="k">select</span>
    <span class="n">yrCovered</span> <span class="k">as</span> <span class="nb">year</span><span class="p">,</span>
    <span class="k">sum</span><span class="p">(</span><span class="n">repOrgsCnt</span><span class="p">)</span> <span class="k">as</span> <span class="nv">"new contracts minimum"</span>
  <span class="k">from</span>
    <span class="n">most_recent_amendment</span>
  <span class="k">where</span>
    <span class="n">formFiled</span> <span class="o">=</span> <span class="s1">'LM-20'</span>
  <span class="k">group</span> <span class="k">by</span>
    <span class="n">yrCovered</span>
<span class="p">),</span>
<span class="n">new_contracts_upper</span> <span class="k">as</span> <span class="p">(</span>
  <span class="k">select</span>
    <span class="n">yrCovered</span> <span class="k">as</span> <span class="nb">year</span><span class="p">,</span>
    <span class="k">sum</span><span class="p">(</span><span class="n">repOrgsCnt</span><span class="p">)</span> <span class="k">as</span> <span class="nv">"new contracts maximum"</span>
  <span class="k">from</span>
    <span class="n">filing</span>
  <span class="k">where</span>
    <span class="n">formFiled</span> <span class="o">=</span> <span class="s1">'LM-20'</span>
  <span class="k">group</span> <span class="k">by</span>
    <span class="n">yrCovered</span>
<span class="p">)</span>
<span class="k">select</span>
  <span class="o">*</span>
<span class="k">from</span>
  <span class="n">new_contracts_lower</span>
  <span class="k">inner</span> <span class="k">join</span> <span class="n">new_contracts_upper</span> <span class="k">using</span><span class="p">(</span><span class="nb">year</span><span class="p">)</span>
<span class="k">where</span> <span class="nb">year</span> <span class="o">&lt;=</span> <span class="mi">2025</span>
<span class="k">order</span> <span class="k">by</span>
  <span class="nb">year</span> <span class="k">desc</span><span class="p">;</span>
</code></pre></div></div>

<p>DuckDB is built for analytic queries and so its dialect is much nicer for this
type of query.</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">WITH</span> <span class="n">ranked</span> <span class="k">AS</span> <span class="p">(</span>
  <span class="k">SELECT</span>
    <span class="o">*</span><span class="p">,</span>
    <span class="n">row_number</span><span class="p">()</span> <span class="n">OVER</span> <span class="p">(</span>
      <span class="k">PARTITION</span> <span class="k">BY</span> <span class="n">srNum</span><span class="p">,</span> <span class="n">beginDate</span><span class="p">,</span> <span class="n">endDate</span><span class="p">,</span> <span class="n">formFiled</span><span class="p">,</span> <span class="n">yrCovered</span>
      <span class="k">ORDER</span> <span class="k">BY</span> <span class="n">amendment</span> <span class="k">DESC</span>
    <span class="p">)</span> <span class="k">AS</span> <span class="n">rn</span>
  <span class="k">FROM</span> <span class="n">filing</span>
  <span class="k">WHERE</span> <span class="n">formFiled</span> <span class="o">=</span> <span class="s1">'LM-20'</span>
<span class="p">)</span>
<span class="k">SELECT</span>
  <span class="n">yrCovered</span> <span class="k">AS</span> <span class="nb">year</span><span class="p">,</span>
  <span class="k">sum</span><span class="p">(</span><span class="n">repOrgsCnt</span><span class="p">)</span> <span class="n">FILTER</span> <span class="p">(</span><span class="k">WHERE</span> <span class="n">rn</span> <span class="o">=</span> <span class="mi">1</span><span class="p">)</span> <span class="k">AS</span> <span class="nv">"new contracts minimum"</span><span class="p">,</span>
  <span class="k">sum</span><span class="p">(</span><span class="n">repOrgsCnt</span><span class="p">)</span> <span class="k">AS</span> <span class="nv">"new contracts maximum"</span>
<span class="k">FROM</span> <span class="n">ranked</span>
<span class="k">GROUP</span> <span class="k">BY</span> <span class="n">yrCovered</span>
<span class="k">HAVING</span> <span class="nb">year</span> <span class="o">&lt;=</span> <span class="mi">2025</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">year</span> <span class="k">DESC</span>
</code></pre></div></div>

<h2 id="speed">Speed</h2>

<p>As for speed, on analytic queries, we have been seeing 10-50× speedups.</p>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[Clearer queries on labordata, and 10-50 times faster]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">LM 30 (and recovered LM 10, 20, &amp;amp; 21)</title>
      <link href="https://notes.labordata.bunkum.us/2026/06/15/lm30" rel="alternate" type="text/html" title="LM 30 (and recovered LM 10, 20, &amp;amp; 21)" />
      <published>2026-06-15T00:00:00+00:00</published>
      <updated>2026-06-15T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2026/06/15/lm30</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2026/06/15/lm30"><![CDATA[<p><a href="https://labordata.bunkum.us">Labordata</a> now has
<a href="https://labordata.bunkum.us/lm30">LM 30 data</a>. These are forms that union
officers and employees are supposed to submit to the Department of Labor if they
have a financial interest or dealing that could pose a conflict of interest.</p>

<p>I’ve not priortized this data previously, but I had to do some major refactors
of the scrapers of the <a href="https://labordata.bunkum.us/lm10">LM 10</a>,
<a href="https://labordata.bunkum.us/lm20">20, and 21</a> data (which are now working
again), and after those changes it was easy to add the LM 30 data, so I have.</p>

<p>Speaking of the LM 20 and 21 data, I was thrilled to see that data used by the
<a href="https://www.epi.org">Economic Policy Institute</a> and
<a href="https://laborlab.us">LaborLab</a> in their
<a href="https://www.epi.org/publication/u-s-employers-spend-more-than-1-5-billion-annually-on-union-avoidance/">report estimating that union avoidance is a $1.5 billiion dollar industry</a>.</p>

<p>If you have used any data from the Labordata project for external publications
or internal analyses, I would be very grateful if you could share that with me!</p>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[New data set on union officer and employee conflict of interests]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">Local union predictor</title>
      <link href="https://notes.labordata.bunkum.us/2026/04/29/labor-parser" rel="alternate" type="text/html" title="Local union predictor" />
      <published>2026-04-29T00:00:00+00:00</published>
      <updated>2026-04-29T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2026/04/29/labor-parser</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2026/04/29/labor-parser"><![CDATA[<p>One of the difficulties of working with data about the labor movement is that it
is often difficult to know which labor organization you are dealing with.</p>

<p><a href="https://github.com/labordata/labor-union-parse/">labor-union-parser</a> is a new
Python package to lookup the a local union’s Office of Management and Labor
Standards (OLMS) filing number from short texts like “United Automobile,
Aerospace and Agricultural Implement Workers of America, UAW Local 1803” and
“LOCAL 6, NEW YORK HOTEL &amp; MOTEL TRADES COUNCIL, UNITE HERE.”</p>

<p>Most labor organizations representing private sector workers are
<a href="https://www.dol.gov/agencies/olms/reports/forms/lm-1-lm-2-lm-3-lm-4">required to file an annual financial report with the Department of Labor’s Office of Management and Labor Standards</a>.
The Office maintains a more or less consistent filing number for unions across
the filings, and so office’s data is the closest we have to a comprehensive
gazette for private-sector-representing labor unions.</p>

<p>This tool uses a probabilistic model to do the lookup, and it’s pretty accurate.
It predicts three things, is a text referring to a union, what is the union
affiliation of the local; what is the filing number of the union.</p>

<table>
  <thead>
    <tr>
      <th>Metric</th>
      <th>Score</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>End-to-End Accuracy</td>
      <td>97.8%</td>
    </tr>
    <tr>
      <td>is union accuracy</td>
      <td>99.2% (4402/4437)</td>
    </tr>
    <tr>
      <td>filing number accuracy</td>
      <td>98.3% (3804/3868)</td>
    </tr>
    <tr>
      <td>union name accuracy</td>
      <td>97.8% (4665/4771)</td>
    </tr>
    <tr>
      <td>Wrong match (union, wrong filing num)</td>
      <td>64</td>
    </tr>
    <tr>
      <td>False negatives (union missed)</td>
      <td>8</td>
    </tr>
    <tr>
      <td>False positives (non-union matched)</td>
      <td>27</td>
    </tr>
  </tbody>
</table>

<p>In our test set, for the records we were able to identify as referring to
particular filing number we get 98.3% accuracy. As we’ll see the performance is
often better than that.</p>

<h2 id="practical-example">Practical example</h2>

<p>I have this wired up into labordata.bunkum.us, where it runs every night against
any column in any table that refers to unions. Here’s
<a href="https://labordata.bunkum.us/_memory?sql=SELECT%0D%0A++CASE%0D%0A++++WHEN+pred_is_union_score+%3C+0.5+THEN+'UNCERTAIN'%0D%0A++++WHEN+pred_union_name_score+%3C+0.5+THEN+'UNCERTAIN'%0D%0A++++ELSE+pred_union_name%0D%0A++END+AS+union_label%2C%0D%0A++COUNT(DISTINCT+case_number)+AS+cnt%0D%0AFROM%0D%0A++[nlrb].filing%0D%0A++INNER+JOIN+[nlrb].participant+USING+(case_number)%0D%0A++INNER+JOIN+[union_names_crosswalk].union_names_crosswalk+ON+[union_names_crosswalk].union_names_crosswalk.union_name+%3D+participant.participant%0D%0AWHERE%0D%0A++date_filed+%3E%3D+'2025-10-01'%0D%0A++AND+date_filed+%3C+'2026'%0D%0A++AND+case_type+%3D+'RC'%0D%0A++AND+subtype+%3D+'Union'%0D%0AGROUP+BY%0D%0A++union_label%0D%0AORDER+BY%0D%0A++union_label+%3D+'UNCERTAIN'%2C%0D%0A++cnt+DESC%3B">a query of the number of distinct election petition cases that unions participated in the last quarter of 2025</a>.</p>

<p>Across the 288 variations of the texts for participants, there is only one
error, and an instructive one. The California State University Employees Union
is mislabeled as belonging to “Field Representatives Union,”
<a href="https://labordata.bunkum.us/opdr-b532245/lm_data?_sort=rpt_id&amp;f_num__exact=517392">the staff union of the California Federation of Teachers</a>.</p>

<p>To this point the California State University Employees Union has been a
public-sector union, and have never filed with the OLMS. This tool will go badly
wrong if the texts include public sector unions. If you have ideas for a good
data source for public sector locals, please let me know.</p>

<h2 id="training-data">Training data</h2>

<p>In the repo, I have put together a training corpus of over 150,000 examples.</p>

<p>The accuracy of this model is the best I’ve been able to achieve, but much
smarter folks than me read these notes. Please use the data to make a better
model.</p>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[What local is this?]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">Labor Union CRM Market Research</title>
      <link href="https://notes.labordata.bunkum.us/2025/02/27/labor-union-crm-market-research" rel="alternate" type="text/html" title="Labor Union CRM Market Research" />
      <published>2025-02-27T00:00:00+00:00</published>
      <updated>2025-02-27T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2025/02/27/labor-union-crm-market-research</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2025/02/27/labor-union-crm-market-research"><![CDATA[<p>The market for CRM or member management for local labor unions in the United
States appears to be unconsolidated. SalesForce and UnionWare seem to capture an
outsize portion of spend, but that appears to be driven by a small number of
expensive systems for very large locals.</p>
<h2 id="methodology" tabindex="-1">Methodology <a class="header-anchor" href="#methodology" aria-hidden="true">#</a></h2>
<p>Labor unions with annual receipts larger than $250,000 are required to fill an
<a href="https://www.dol.gov/sites/dolgov/files/OLMS/regs/compliance/GPEA_Forms/forms/lm2_form_facsimile_2022.pdf">LM-2
form</a>
and submit it to the Department of Labor’s Office of Labor-Management Standards
yearly.</p>
<p>We use this form to find unions that are using a CRM product and how much they
are spending on it.</p>
<p>Here’s our procedure. For each local union that files LM-2 forms, we look at the
forms filed starting in 2010 for mentions of the CRM company or product in the
disbursement or purchases schedules (schedules 18 and 4, respectively). If a union
has multiple years in which a CRM company or product were mentioned, we just look
at the most recent form. If more than one company is mentioned, we choose the
company that received more money from the union.</p>
<p>This method definitely under-counts the number of unions that are using a CRM
product and how much they are paying for it, but it is difficult to evaluate the
extent of the error. In June, 2022, UnionWare claimed to serve <a href="https://web.archive.org/web/20220626194835/https://www.unionware.com/products">158 unions across
three
countries</a>.
Our methodology has finds 43. If their number is still accurate, then our estimate
for UnionWare is off by at most 267%.</p>
<p>The CRM companies and products are listed below:</p>
<table>
<thead>
<tr>
<th>Company</th>
<th>Product</th>
</tr>
</thead>
<tbody>
<tr>
<td>UnionWare</td>
<td><a href="https://www.unionware.com/">UnionWare</a></td>
</tr>
<tr>
<td>SalesForce</td>
<td><a href="https://www.salesforce.com/">SalesForce</a></td>
</tr>
<tr>
<td><a href="https://www.winmill.com/">Winmill Software</a></td>
<td><a href="https://emembership.winmill.com">eMembership</a></td>
</tr>
<tr>
<td><a href="https://www.rcsunionsoftware.com/">RCS Union Software</a></td>
<td></td>
</tr>
<tr>
<td>Aptify</td>
<td><a href="https://www.aptify.com/">Aptify</a></td>
</tr>
<tr>
<td>union.dev</td>
<td><a href="https://union.dev/">union.dev</a></td>
</tr>
<tr>
<td><a href="https://www.advsol.com">Advanced Solutions International</a></td>
<td><a href="https://www.imis.com/">iMIS</a></td>
</tr>
<tr>
<td>UnionTrack</td>
<td><a href="https://www.uniontrack.com/">UnionTrack</a></td>
</tr>
<tr>
<td><a href="https://www.paragoncorporation.com">Paragon Corporation</a></td>
<td><a href="https://www.paragoncorporation.com/union">MemTrack</a></td>
</tr>
<tr>
<td>Union Digital</td>
<td><a href="https://union1software.com/">UN1ON</a></td>
</tr>
<tr>
<td><a href="https://www.netincom.net/">INCOM Integrated Computer Systems</a></td>
<td><a href="https://www.membershiptrackingprogram.com/">MTP</a></td>
</tr>
<tr>
<td>Union365</td>
<td><a href="https://union365.ca/">Union365</a></td>
</tr>
<tr>
<td><a href="https://www.kmrsystems.com/">KMR Systems</a></td>
<td></td>
</tr>
<tr>
<td><a href="https://strategicorganizingsystems.com/salesforce/">Strategic Organizing Systems</a></td>
<td></td>
</tr>
</tbody>
</table>
<p>Notes:</p>
<p>A number of other vendors do sell CRMs, but their CRMs are much less popular than
their other offerings. Since the LM data typically does not allow us to identify
which product was bought from a vendor, we exclude these vendors: Microsoft,
Oracle, and <a href="https://www.rcsunionsoftware.com/">RCS Union Software</a>.</p>

<div id="cell-1" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-11-13-new-contracts-lm-20/cell-1.png" alt="chart" style="max-width:100%"></div>

<div id="cell-2" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-12-11-petitions-to-date/cell-2.png" alt="chart" style="max-width:100%"></div>

<p>If we segment the locals by size into three groups (small: 0-999, medium:
1,000-9,999, large: 10,000+), we can see that SalesForce’s and UnionWare’s spend
is driven by the large locals.</p>

<div id="cell-4" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-12-11-petitions-to-date/cell-4.png" alt="chart" style="max-width:100%"></div>

<div id="cell-5" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-08-24-improving-certification-success/cell-5.png" alt="chart" style="max-width:100%"></div>

<p>Here is the underlying data behind these charts:</p>

<div id="cell-7" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2025-02-27-labor-union-crm-market-research/cell-7.png" alt="chart" style="max-width:100%"></div>

<div id="cell-8" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-08-24-improving-certification-success/cell-8.png" alt="chart" style="max-width:100%"></div>

<div id="cell-9" class="reactive-cell"></div>

<script type="module">
import {define, main, Plot, Inputs, d3} from "/assets/js/reactive-runtime.js";
// Register Plot, Inputs, and d3 as reactive variables so cells that
// reference them resolve through the dependency graph (not runtime builtins).
main.variable().define("Plot", [], () => Plot);
main.variable().define("Inputs", [], () => Inputs);
main.variable().define("d3", [], () => d3);
// Hydrate an inline ${…}: bind its reactive value to its placeholder span,
// updating only that span — never re-render the surrounding (static) prose.
function hydrate(ref, inputs, body) {
  const el = document.querySelector(`[data-reactive="${ref}"]`);
  if (!el) return;
  main.variable({
    pending() {},
    rejected(error) { console.error(error); },
    fulfilled(value) {
      el.replaceChildren(value instanceof Node ? value : document.createTextNode(value == null ? "" : String(value)));
    }
  }).define(null, inputs, body);
}
define(
  {root: document.getElementById(`cell-1`), expanded: [], variables: []},
  {
    id: 1,
    body: (display,Plot,crm_market) => {
display(
  Plot.plot({
    title: "Estimated Annual Spend",
    marginBottom: 100,
    y: {
      transform: (d) => d / 1000000,
      label: "↑ Spend (US dollars, millions)",
      grid: 5,
    },
    x: {
      tickRotate: -30,
    },
    marks: [
      Plot.barY(crm_market, {
        x: "company",
        y: "spend",
        sort: { x: "y", reverse: true },
      }),
      Plot.ruleY([0]),
    ],
  }),
);
},
    inputs: ["display","Plot","crm_market"],
    outputs: [],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-2`), expanded: [], variables: []},
  {
    id: 2,
    body: (display,Plot,crm_market) => {
display(
  Plot.plot({
    title: "Estimated Local Union Clients",
    marginBottom: 100,
    x: {
      tickRotate: -30,
      label: "Product",
    },
    y: { label: "↑ Number of locals" },
    marks: [
      Plot.barY(crm_market, {
        x: "company",
        y: "cnt",
        sort: { x: "y", reverse: true },
      }),
      Plot.ruleY([0]),
    ],
  }),
);
},
    inputs: ["display","Plot","crm_market"],
    outputs: [],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-4`), expanded: [], variables: []},
  {
    id: 4,
    body: (display,Plot,crm_market) => {
display(
  Plot.plot({
    title: "Estimated Annual Spend, Segmented by Local Size",
    marginBottom: 100,
    marginRight: 50,
    y: {
      transform: (d) => d / 1000000,
      label: "↑ Spend (US dollars, millions)",
      grid: 5,
    },
    fy: { label: null },
    x: {
      tickRotate: -30,
    },
    marks: [
      Plot.barY(crm_market, {
        x: "company",
        y: "spend",
        fy: "size",
        sort: { x: "y", reverse: true },
      }),
      Plot.ruleY([0]),
    ],
  }),
);
},
    inputs: ["display","Plot","crm_market"],
    outputs: [],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-5`), expanded: [], variables: []},
  {
    id: 5,
    body: (display,Plot,crm_market) => {
display(
  Plot.plot({
    title: "Estimated Local Union Clients, Segmented by Local Size",
    marginBottom: 100,
    marginRight: 50,
    x: {
      tickRotate: -30,
      label: "Product",
    },
    fy: { label: null },
    y: { label: "↑ Number of locals" },
    marks: [
      Plot.barY(crm_market, {
        x: "company",
        y: "cnt",
        fy: "size",
        sort: { x: "y", reverse: true },
      }),
      Plot.ruleY([0]),
    ],
  }),
);
},
    inputs: ["display","Plot","crm_market"],
    outputs: [],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-7`), expanded: [], variables: []},
  {
    id: 7,
    body: async (display,crm_market) => {
const {table} = await import("/assets/js/toms-table.js").then((module) => {
  if (!("table" in module)) throw new SyntaxError(`export 'table' not found`);
  return module;
});
display(
  table(
    crm_market
      .filter((d) => d.company)
      .sort((a, b) => b.spend - a.spend)
      .map((d) => ({
        Product: d.company,
        "Local size": d.size,
        Locals: d.cnt,
        "Estimated annual spend": d.spend,
      })),
    { title: "Estimated CRM market, by product and local size" },
  ),
);
return {table};
},
    inputs: ["display","crm_market"],
    outputs: ["table"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-8`), expanded: [], variables: []},
  {
    id: 8,
    body: async () => {
const {DatasetteClient} = await import("/assets/js/datasette-client.js").then((module) => {
  if (!("DatasetteClient" in module)) throw new SyntaxError(`export 'DatasetteClient' not found`);
  return module;
});
const db = new DatasetteClient("https://labordata.bunkum.us/opdr");
return {DatasetteClient,db};
},
    inputs: [],
    outputs: ["DatasetteClient","db"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-9`), expanded: [], variables: []},
  {
    id: 9,
    body: async (db) => {
const vendorPattern =
  "salesforce|aptify|unionware|winmill|union[.]dev|advanced solutions|uniontrack|union track|paragon corp|integrated comp|kmr system|strategic organizing system|john sladkus";

const crm_market = await db.query`
with matched as (
  -- disbursements whose payee or purpose names a known CRM vendor
  SELECT
    name,
    purpose,
    coalesce(itemized, 0) + coalesce(non_itemized, 0) as amount,
    ar_payer_payee.rpt_id
  FROM
    ar_payer_payee
    left join ar_disbursements_genrl using (payer_payee_id)
  WHERE
    payer_payee_type = 1002
    and (
      regexp_matches(lower(name), ${vendorPattern})
      OR lower(name) = 'incom'
      OR regexp_matches(lower(purpose), 'salesforce|unionware|aptify|uniontrack')
      OR (
        regexp_matches(lower(purpose), 'imis')
        and not regexp_matches(lower(purpose), 'adimis')
      )
    )
  UNION ALL
  SELECT
    description,
    null as purpose,
    cash_paid,
    rpt_id
  FROM
    ar_disbursements_inv_purchases
  WHERE
    regexp_matches(lower(description), ${vendorPattern})
    OR lower(description) = 'incom'
),
most_recent as (
  -- attribute each expenditure to a vendor, keeping only each local's most
  -- recent filing (highest spend breaks ties)
  SELECT
    members,
    amount,
    CASE
      WHEN regexp_matches(lower(purpose), 'salesforce') OR regexp_matches(lower(name), 'salesforce') THEN 'SalesForce'
      WHEN regexp_matches(lower(purpose), 'unionware') OR regexp_matches(lower(name), 'unionware') THEN 'UnionWare'
      WHEN regexp_matches(lower(name), 'winmill') THEN 'eMembership'
      WHEN regexp_matches(lower(name), 'union[.]dev') THEN 'union.dev'
      WHEN regexp_matches(lower(purpose), 'imis') OR regexp_matches(lower(name), 'advanced solutions') THEN 'iMIS'
      WHEN regexp_matches(lower(purpose), 'uniontrack') OR regexp_matches(lower(name), 'uniontrack') OR regexp_matches(lower(name), 'union track') THEN 'UnionTrack'
      WHEN regexp_matches(lower(name), 'paragon corp') THEN 'MemTrack'
      WHEN regexp_matches(lower(name), 'integrated comp') OR lower(name) = 'incom' THEN 'MTP'
      WHEN regexp_matches(lower(name), 'kmr system') THEN 'KMR Systems'
      WHEN regexp_matches(lower(name), 'strategic organizing system') OR regexp_matches(lower(name), 'john sladkus') THEN 'Strategic Organizing System'
      WHEN regexp_matches(lower(purpose), 'aptify') OR regexp_matches(lower(name), 'aptify') THEN 'Aptify'
    END AS company
  FROM
    matched
    inner join lm_data using (rpt_id)
  WHERE
    pd_covered_from >= '2010-01-01'
    AND desig_name = 'LU'
  QUALIFY
    row_number() OVER (
      PARTITION BY f_num
      ORDER BY pd_covered_from DESC, amount DESC
    ) = 1
)
SELECT
  CASE
    WHEN members < 1000 THEN 'small'
    WHEN members < 10000 THEN 'medium'
    WHEN members >= 10000 THEN 'large'
  END as size,
  company,
  count(*) as cnt,
  sum(amount) as spend
FROM most_recent
GROUP BY size, company`;
return {vendorPattern,crm_market};
},
    inputs: ["db"],
    outputs: ["vendorPattern","crm_market"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

</script>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[Which CRM / member-management vendors local unions buy, and how much they spend, from LM-2 filings.]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">Counties for CHIPS and FMCS Deadend</title>
      <link href="https://notes.labordata.bunkum.us/2024/04/08/chips-update" rel="alternate" type="text/html" title="Counties for CHIPS and FMCS Deadend" />
      <published>2024-04-08T00:00:00+00:00</published>
      <updated>2024-04-08T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2024/04/08/chips-update</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2024/04/08/chips-update"><![CDATA[<p>Couple of quick updates.</p>

<h2 id="transcribed-county-table-for-chips-dataset">Transcribed County Table for CHIPS dataset</h2>

<p><a href="https://www.linkedin.com/in/sean-brailey-a4ab49284">Sean Brailey</a>, Contract Data Engineer, <a href="https://lerc.uoregon.edu/">Labor Education and Research Center</a>, transcribed the <a href="https://labordata.bunkum.us/chips-11c5680/l_county"><code class="language-plaintext highlighter-rouge">l_county</code></a> table from the <a href="https://s3.amazonaws.com/NARAprodstorage/opastorage/live/1/8902/890201/content/arcmedia/electronic-records/rg-025/chips/113.1CL.pdf">codebook</a> for the CHIPS dataset.</p>

<p>The CHIPS dataset contains information about NLRB cases from
1984-2000, and with Sean’s work we can now quickly see the <a href="https://labordata.bunkum.us/chips-11c5680?sql=select%0D%0A++county_name+as+county%2C%0D%0A++alpha_state_code+as+state%2C%0D%0A++count%28*%29+as+rc_cases%0D%0Afrom%0D%0A++r_master%0D%0A++inner+join+l_county+on+county+%3D+county_number%0D%0A++and+state+%3D+numeric_state_code%0D%0A++and+ctype+%3D+%27RC%27%0D%0Agroup+by%0D%0A++county_number%2C%0D%0A++numeric_state_code%0D%0Aorder+by%0D%0A++count%28*%29+desc">top five
counties for RC cases in those sixteen years</a>: Cook, IL; Los Angeles, CA; New York; NY; Wayne, MI.</p>

<p>Previously, we had the <a href="https://labordata.bunkum.us/chips-11c5680/r_address">addresses of cases</a>, so this type of analysis was
possible before, but with much more effort.</p>

<h2 id="fmcs-foia-deadend">FMCS FOIA deadend</h2>

<p>It looks like our long effort to try to get more unique identifiers for unions and
employers for the F7 Intent to Bargain notices has run its course. According the the
FOIA officer, the fields we want from their case management system are not used
even though they are present in their database schema.</p>

<p>So, back to record linkage.</p>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[Thanks, Sean!]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">Union President Turnover</title>
      <link href="https://notes.labordata.bunkum.us/2024/04/02/union-president-turnover" rel="alternate" type="text/html" title="Union President Turnover" />
      <published>2024-04-02T00:00:00+00:00</published>
      <updated>2024-04-02T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2024/04/02/union-president-turnover</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2024/04/02/union-president-turnover"><![CDATA[<p>Local unions are required to file information about their organization and
finances every year to the Department of Labor’s Office of Management and Labor
Standards (OLMS). These forms include information about the officers of the
union.</p>
<p>We can use the information from these forms to make a measurement of how often
the leadership of local union changes.</p>
<p>We would like to know the rate at which presidents of a local union turns over.
We can approximate the rate of changes in presidents by counting the number of
distinct last names that are listed for a president in a yearly filing divided by
the number of yearly filings submitted by the union. In order to correct for
biases we subtract one from both the top and the bottom.</p>
<p><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mtext>Turnover</mtext><mo>∼</mo><mfrac><mrow><mtext>Distinct names</mtext><mo>−</mo><mn>1</mn></mrow><mrow><mtext>Years</mtext><mo>−</mo><mn>1</mn></mrow></mfrac></mrow><annotation encoding="application/x-tex">\text{Turnover} \sim \frac{\text{Distinct names} - 1}{\text{Years} - 1}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6833em;"></span><span class="mord text"><span class="mord">Turnover</span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">∼</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:2.1297em;vertical-align:-0.7693em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3603em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord text"><span class="mord">Years</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord">1</span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord text"><span class="mord">Distinct names</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord">1</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.7693em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p>
<p>If the the last name is very common, or if presidency passed from one relative to
another, then our measure will underestimate turnover. If the filing has a
misspelling of the president’s last name, then we will overestimate.</p>
<p>Generally, though, our measure seems to be a reasonable approximation of
presidential turnover.</p>
<p>One way, we can check is to see if our measure performs as we would expect from
the literature on local union democracy.</p>
<p>In J.C. Anderson’s 1979 article, <a href="https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1468-232X.1978.tb00138.x">“A Comparative Analysis of Local Union
Democracy”</a>,
Anderson identified a number of factors that are correlated with measures of
local union democracy. In particular, he found that larger unions and
long-established unions tended to have less competitive elections for officers.</p>
<p>Competitive elections for union officers should have a strong relation with
turnover of officers. Losing elections is one direct cause of turnover. Similarly
the fear of losing, or unwillingness to fight a difficult campaign also lead
incumbent officers to decide to not run again.</p>
<h2 id="union-size" tabindex="-1">Union Size <a class="header-anchor" href="#union-size" aria-hidden="true">#</a></h2>
<p>As expected, larger local unions do have lower turnover of presidents.</p>

<div id="cell-1" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-11-13-new-contracts-lm-20/cell-1.png" alt="chart" style="max-width:100%"></div>

<h2 id="age-of-union" tabindex="-1">Age of Union <a class="header-anchor" href="#age-of-union" aria-hidden="true">#</a></h2>
<p>And younger locals do have higher presidential turnover.</p>
<p>This chart only include locals started after December 1, 1970. The data from the
OLMS does not include establishment data for local unions established before that
date.</p>

<div id="cell-3" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-08-24-improving-certification-success/cell-3.png" alt="chart" style="max-width:100%"></div>

<div id="cell-4" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-12-11-petitions-to-date/cell-4.png" alt="chart" style="max-width:100%"></div>

<div id="cell-5" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-08-24-improving-certification-success/cell-5.png" alt="chart" style="max-width:100%"></div>

<h1>Conclusion</h1>
<p>At least in aggregate, our measure of presidential turnover seems reasonable. All
the relationships shown here are still substantial and statistically significant
in a multiple regression analysis.</p>
<p>It could be further refined by stricter checking that a difference in last names
means the person is different or the sharing of last names means the person is
the same. Using first name and a fuzzy string distance to allow for some typos
would be good next steps.</p>
<p>For modelling, logistic regression is likely the most appropriate choice. Every
year would be a trial with a binary outcome of whether this years president is the
same or different than last year. This modeling choice would handle different
number of available years naturally. This modeling would also allow for
year-varying features.</p>
<h2 id="suggestions" tabindex="-1">Suggestions <a class="header-anchor" href="#suggestions" aria-hidden="true">#</a></h2>
<p>From twitter, I got a number of really useful suggestions. Where the tweet was
public or I have permission, I will cite the user.</p>
<p>First, someone rightfully pointed out that presidents can change while the faction
in power remains the same. It could be really useful to compare the entire set of
officers in one year to the entire set officer in the next. If many or all
officers changed, that would be a much better signal that the union has
meaningfully different leadership than if just the president changed. I think this
idea could really improve the precision of the measure.</p>
<p>One way you could implement:</p>
<p><span class="katex-display"><span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mfrac><mrow><mtext>Distinct Officer Names</mtext><mo>−</mo><mtext>Distinct Officer Positions</mtext></mrow><mrow><mo stretchy="false">(</mo><mtext>Years</mtext><mo>−</mo><mn>1</mn><mo stretchy="false">)</mo><mo>⋅</mo><mtext>Distinct Officer Positions</mtext></mrow></mfrac></mrow><annotation encoding="application/x-tex">\frac{\text{Distinct Officer Names} - \text{Distinct Officer Positions}}{(\text{Years} - 1) \cdot \text{Distinct Officer Positions}}</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:2.3074em;vertical-align:-0.936em;"></span><span class="mord"><span class="mopen nulldelimiter"></span><span class="mfrac"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:1.3714em;"><span style="top:-2.314em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mopen">(</span><span class="mord text"><span class="mord">Years</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord">1</span><span class="mclose">)</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">⋅</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord text"><span class="mord">Distinct Officer Positions</span></span></span></span><span style="top:-3.23em;"><span class="pstrut" style="height:3em;"></span><span class="frac-line" style="border-bottom-width:0.04em;"></span></span><span style="top:-3.677em;"><span class="pstrut" style="height:3em;"></span><span class="mord"><span class="mord text"><span class="mord">Distinct Officer Names</span></span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mord text"><span class="mord">Distinct Officer Positions</span></span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.936em;"><span></span></span></span></span></span><span class="mclose nulldelimiter"></span></span></span></span></span></span></p>
<p>This is still a bit mushy. Would not distinguish a pattern of officers change that
has a high level of continuity</p>
<table>
<thead>
<tr>
<th>year</th>
<th>president</th>
<th>vice-president</th>
<th>treasurer</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>alice</td>
<td>bob</td>
<td>candice</td>
</tr>
<tr>
<td>2</td>
<td>alice</td>
<td>bob</td>
<td>dawn</td>
</tr>
<tr>
<td>3</td>
<td>alice</td>
<td>earl</td>
<td>frank</td>
</tr>
</tbody>
</table>
<p>versus many years of complete continuity and then a sudden change.</p>
<table>
<thead>
<tr>
<th>year</th>
<th>president</th>
<th>vice-president</th>
<th>treasurer</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>alice</td>
<td>bob</td>
<td>candice</td>
</tr>
<tr>
<td>2</td>
<td>alice</td>
<td>bob</td>
<td>candice</td>
</tr>
<tr>
<td>3</td>
<td>dawn</td>
<td>earl</td>
<td>frank</td>
</tr>
</tbody>
</table>
<p>Second, <a href="https://bsky.app/profile/dskamper.bsky.social">Dave Kamper</a> suggested that turnover should
correlate with:</p>
<ul>
<li>salary of president (I bet this correlates strongly with size of union)</li>
<li>whether the local has paid staff</li>
<li>the financial trajectory of the union</li>
<li>the membership trajectory</li>
</ul>
<h2 id="appendix" tabindex="-1">Appendix <a class="header-anchor" href="#appendix" aria-hidden="true">#</a></h2>

<div id="cell-7" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2025-02-27-labor-union-crm-market-research/cell-7.png" alt="chart" style="max-width:100%"></div>

<div id="cell-8" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-08-24-improving-certification-success/cell-8.png" alt="chart" style="max-width:100%"></div>

<div id="cell-9" class="reactive-cell"></div>

<div id="cell-10" class="reactive-cell"><img src="https://notes.labordata.bunkum.us/assets/snapshots/2021-08-24-improving-certification-success/cell-10.png" alt="chart" style="max-width:100%"></div>

<script type="module">
import {define, main, Plot, Inputs, d3} from "/assets/js/reactive-runtime.js";
// Register Plot, Inputs, and d3 as reactive variables so cells that
// reference them resolve through the dependency graph (not runtime builtins).
main.variable().define("Plot", [], () => Plot);
main.variable().define("Inputs", [], () => Inputs);
main.variable().define("d3", [], () => d3);
// Hydrate an inline ${…}: bind its reactive value to its placeholder span,
// updating only that span — never re-render the surrounding (static) prose.
function hydrate(ref, inputs, body) {
  const el = document.querySelector(`[data-reactive="${ref}"]`);
  if (!el) return;
  main.variable({
    pending() {},
    rejected(error) { console.error(error); },
    fulfilled(value) {
      el.replaceChildren(value instanceof Node ? value : document.createTextNode(value == null ? "" : String(value)));
    }
  }).define(null, inputs, body);
}
define(
  {root: document.getElementById(`cell-1`), expanded: [], variables: []},
  {
    id: 1,
    body: (regressionLoess,local_unions,display,Plot) => {
const sizeLoess = regressionLoess()
  .x((d) => Math.log10(d.members))
  .y((d) => d.turn_over_rate)
  .bandwidth(0.5)(local_unions);
display(
  Plot.plot({
    y: { label: "Changes in presidents per decade" },
    x: {
      tickFormat: (x) => 10 ** x,
      ticks: 5,
      label: "Members in local (log scale)",
    },
    caption: "Turnover by Size of Local Union",
    marks: [
      Plot.rect(
        local_unions,
        Plot.bin(
          { fillOpacity: "count" },
          {
            x: (d) => Math.log10(d.members),
            y: "turn_over_rate",
            fill: "lightblue",
          },
        ),
      ),
      Plot.line(sizeLoess, { x: (d) => d[0], y: (d) => d[1] }),
    ],
  }),
);
return {sizeLoess};
},
    inputs: ["regressionLoess","local_unions","display","Plot"],
    outputs: ["sizeLoess"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-3`), expanded: [], variables: []},
  {
    id: 3,
    body: (regressionLoess,local_unions_post_70,d3,display,Plot) => {
const ageLoess = regressionLoess()
  .x((d) => +d.est_year)
  .y((d) => d.turn_over_rate)
  .bandwidth(0.5)(local_unions_post_70)
  .map(([x, y]) => [d3.utcParse("%Y")(String(Math.round(x))), y]);
display(
  Plot.plot({
    y: { label: "Changes in president per decade" },
    x: { label: "Year local established" },
    caption: "Turnover by Year the Local Union Was Established",
    marks: [
      Plot.rect(
        local_unions_post_70,
        Plot.bin(
          { fillOpacity: "count" },
          {
            x: (d) => d3.utcParse("%Y")(d.est_year),
            y: "turn_over_rate",
            fill: "lightblue",
          },
        ),
      ),
      Plot.line(ageLoess, { x: (d) => d[0], y: (d) => d[1] }),
    ],
  }),
);
return {ageLoess};
},
    inputs: ["regressionLoess","local_unions_post_70","d3","display","Plot"],
    outputs: ["ageLoess"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-4`), expanded: [], variables: []},
  {
    id: 4,
    body: (display,md,affiliations) => {
display(md`## National Union

The factor that most strongly relates to presidential turnover was not in Anderson's study: the affiliation of the local union with a national union.

Locals affiliated with the National Staff Organization, a union of union staff, change their presidents, on average, ${affiliations.find((row) => row["National union"] === "NSOI")["Average changes of local presidents per decade"]} times per decade, while United Mine Workers locals, on average, change presidents ${affiliations.find((row) => row["National union"] === "UMW")["Average changes of local presidents per decade"]} times per decade.`);
},
    inputs: ["display","md","affiliations"],
    outputs: [],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-5`), expanded: [], variables: []},
  {
    id: 5,
    body: async (display,affiliations) => {
const {table} = await import("/assets/js/toms-table.js").then((module) => {
  if (!("table" in module)) throw new SyntaxError(`export 'table' not found`);
  return module;
});
display(table(affiliations, { title: "Turnover by National Union" }));
return {table};
},
    inputs: ["display","affiliations"],
    outputs: ["table"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-7`), expanded: [], variables: []},
  {
    id: 7,
    body: async () => {
const [{regressionLoess}, {DatasetteClient}] = await Promise.all([import("https://cdn.jsdelivr.net/npm/d3-regression@1/+esm").then((module) => {
  if (!("regressionLoess" in module)) throw new SyntaxError(`export 'regressionLoess' not found`);
  return module;
}), import("/assets/js/datasette-client.js").then((module) => {
  if (!("DatasetteClient" in module)) throw new SyntaxError(`export 'DatasetteClient' not found`);
  return module;
})]);
const db = new DatasetteClient("https://labordata.bunkum.us/opdr");
return {regressionLoess,DatasetteClient,db};
},
    inputs: [],
    outputs: ["regressionLoess","DatasetteClient","db"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-8`), expanded: [], variables: []},
  {
    id: 8,
    body: async (db) => {
const affiliations = await db.query`WITH presidents AS (
  SELECT
    DISTINCT ON (f_num, yr_covered) *
  FROM
    ar_disbursements_emp_off
    JOIN lm_data USING (rpt_id)
  WHERE
    title = 'PRESIDENT'
    AND desig_name = 'LU'
  ORDER BY
    f_num,
    yr_covered
),
turnover AS (
  SELECT
    f_num,
    any_value(aff_abbr) AS aff_abbr,
    min(est_date) AS est_date,
    year(min(est_date)) <= 1970 AS established_before_1971,
    avg(members) AS members,
    (count(DISTINCT last_name) - 1) * 10 / (count(*) - 1) AS turn_over_rate
  FROM
    presidents
  GROUP BY
    f_num
  HAVING
    count(*) >= 3
    AND avg(members) > 0
)
SELECT
  aff_abbr AS "National union",
  round(avg(turn_over_rate), 1) AS "Average changes of local presidents per decade",
  round(quantile_disc(turn_over_rate, 0.25), 1) AS "25th percentile",
  round(quantile_disc(turn_over_rate, 0.75), 1) AS "75th percentile",
  count(*) AS "Number of locals"
FROM
  turnover
GROUP BY
  aff_abbr
HAVING
  count(*) >= 10
ORDER BY
  avg(turn_over_rate) DESC`;
return {affiliations};
},
    inputs: ["db"],
    outputs: ["affiliations"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-9`), expanded: [], variables: []},
  {
    id: 9,
    body: async (db) => {
const local_unions = await db.query`WITH presidents AS (
  SELECT
    distinct on (f_num, yr_covered) *
  FROM
    ar_disbursements_emp_off
    INNER JOIN lm_data USING (rpt_id)
  WHERE
    title = 'PRESIDENT'
    AND desig_name = 'LU'
  ORDER BY
    f_num,
    yr_covered
)
SELECT
  f_num,
  any_value(aff_abbr) as aff_abbr,
  nullif (min(est_date), DATE '1970-12-01') AS est_date,
  year(min(est_date)) < 1971 established_before_1971,
  avg(members) AS members,
  (count(DISTINCT last_name) - 1) * 10 / (count(*) - 1) AS turn_over_rate
FROM
  presidents
GROUP BY
  f_num
HAVING
  count(*) >= 3
  AND avg(members) > 0`;
return {local_unions};
},
    inputs: ["db"],
    outputs: ["local_unions"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

define(
  {root: document.getElementById(`cell-10`), expanded: [], variables: []},
  {
    id: 10,
    body: async (db) => {
const local_unions_post_70 = await db.query`WITH presidents AS (
  SELECT DISTINCT ON (f_num, yr_covered) *
  FROM ar_disbursements_emp_off
  JOIN lm_data USING (rpt_id)
  WHERE title = 'PRESIDENT'
    AND desig_name = 'LU'
    AND est_date != DATE '1970-12-01'
  ORDER BY f_num, yr_covered
)
SELECT
  f_num,
  any_value(aff_abbr) AS aff_abbr,
  year(min(est_date)) AS est_year,
  avg(members) AS members,
  (count(DISTINCT last_name) - 1) * 10 / (count(*) - 1) AS turn_over_rate
FROM presidents
GROUP BY f_num
HAVING count(*) >= 3
  AND avg(members) IS NOT NULL
  AND count(*) FILTER (WHERE year(est_date) > yr_covered) <= 1
ORDER BY est_year DESC`;
return {local_unions_post_70};
},
    inputs: ["db"],
    outputs: ["local_unions_post_70"],
    output: null,
    autodisplay: false,
    autoview: false,
    automutable: false
  }
);

</script>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[Approximating how often local union presidents change, and what predicts it.]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">LM-10 Forms, NLRB Case Employer Addresses, and More</title>
      <link href="https://notes.labordata.bunkum.us/2023/12/04/lm10s" rel="alternate" type="text/html" title="LM-10 Forms, NLRB Case Employer Addresses, and More" />
      <published>2023-12-04T00:00:00+00:00</published>
      <updated>2023-12-04T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2023/12/04/lm10s</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2023/12/04/lm10s"><![CDATA[<h2 id="lm-10-forms">LM-10 Forms</h2>
<p>Employers who hire labor relations consultants (union-busters) or who have financial dealing with union officials have to file a LM-10 form with the Department of Labor disclosing that activity.</p>

<p>The <a href="https://labordata.bunkum.us/lm10">database now has these LM-10 filings</a>, updated nightly.</p>

<p>These are a good complement to the <a href="https://labordata.bunkum.us/lm20">LM-20 and LM-21 forms</a> that we already scrape nightly.</p>

<p>One shortcoming of these forms is that they are all at the firm level. They do not disclose information about the specific establishment where the employers are trying to suppress unionization.</p>

<p>It would be good to get these forms amended so they did include that information.</p>

<h2 id="voluntary-recognitions">Voluntary Recognitions</h2>
<p>We have added the <a href="https://labordata.bunkum.us/voluntary_recognitions">NLRB voluntary recognitions notifications</a> for the second and third quarter of 2023.</p>

<p>The NLRB was planning on rescinding the Trump-era rule that the NLRB had to be given notice of a voluntary recognition in order for union to enjoy a period of protection from an NLRB certification election.  It looks like the <a href="https://www.reginfo.gov/public/do/eAgendaViewRule?pubId=202304&amp;RIN=3142-AA22">new rule was supposed to be finalized this fall</a>, but for some reason it looks
it has not. Does anyone have an idea about why not?</p>

<p>I’ll keep on asking for these notices quarterly, until the data stops.</p>

<h2 id="nlrb-participant-addresses">NLRB Participant Addresses</h2>
<p>The NLRB’s website lists participants in a case, but does not provide the full address of the employer and unions involved, just the city, state and zip code.</p>

<p>I submitted a <a href="https://www.muckrock.com/foi/united-states-of-america-10/participant-and-participant_party-tables-151172/">FOIA request</a>, and now we have the full address for participating. employers and unions for all case in the NxGen system through September 15, 2023.</p>

<p>These data are useful for identifying the particular establishment of the employer, not just firm, and for figuring out which union local was involved in an NLRB case.</p>

<p>I have updated the <a href="https://labordata.bunkum.us/nlrb/participant">participant table</a> of the nlrb database with these more complete addresses.</p>

<p>In the course of making these improvements, I found that I wasn’t fully capturing the role of participants (petitioner, intervener, etc), and that I was even missing some participants in the database. The scraping schedule has been adjusted to prioritize fixing these data issues, and they should be addressed in a few weeks.</p>

<p>Note: the names and addresses of individual persons who are parties to NLRB cases have been redacted.</p>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[Data updates]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">NLRB and FMCS case management schemas</title>
      <link href="https://notes.labordata.bunkum.us/2023/04/03/schemas" rel="alternate" type="text/html" title="NLRB and FMCS case management schemas" />
      <published>2023-04-03T00:00:00+00:00</published>
      <updated>2023-04-03T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2023/04/03/schemas</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2023/04/03/schemas"><![CDATA[<h1 id="fmcs-case-management-system">FMCS case management system</h1>
<p><a href="https://www.jsvine.com/">Jeremy Singer-Vine</a> published a public
records request response from the Federal Mediation and Conciliation
Service (FMCS) that provides <a href="https://github.com/data-liberation-project/fmcs-work-stoppage-records">the schema for the service’s case
management
system</a>.</p>

<p>These records show all the different tables (i.e. spreadsheets) that
the case management software uses to tracks data. For each table, the
records list the column names.</p>

<p>Using a schema, you can get a pretty good idea of all the information
that the FMCS tracks digitally.</p>

<p>I was very interested in it because it appears that the at least
employers and maybe bargaining units have unique IDs in their system.</p>

<p>That would mean it would be possible to get longitudinal dataset of
bargaining units from the record of F-7: Notice of Intent to Bargain
forms.</p>

<p>I’ve thought about that dataset for a while and have started on the
fuzzy matching to try to build it, but if we can get it straight from
the FMCS that would be much better.</p>

<p>If you know anybody that has familiarity with the FMCS’s data systems,
it would be great to talk to them on background. If you have ideas, please
let me know.</p>

<h1 id="nlrb-nxgen-case-management-schema">NLRB NxGen case management schema</h1>
<p>Looking through the data exports from the <a href="/2023/02/01/39-years-of-ulp">previous NLRB case
management systems</a>,
there’s a lot of interesting fields that are not available for current
cases through the NLRB website (industrial classification is the one that
makes my heart ache).</p>

<p>I put together a public records request for the schema of the NLRB’s
NxGen case management system, and <a href="https://www.muckrock.com/foi/united-states-of-america-10/schema-or-table-definitions-for-the-nxgen-case-management-system-139981/">they fulfilled it</a>.</p>

<p>The case information that is available on the NLRB website is a subset
of the data in the NxGen system, and so the schema can be a useful
guide to finding information that the NLRB tracks but does not
routinely publish. Much of that unpublished data should be accessible
through a public records request.</p>

<h1 id="first-quarter-of-nlrb-filings">First Quarter of NLRB filings</h1>
<p>The first quarter of 2023 looks a lot like the first quarter
of 2022. While the scale of organizing is still not near enough, NLRB
organizing appears to be continuing at a levels we haven’t seen since the 2000s.</p>

<iframe width="100%" height="492" frameborder="0" src="https://observablehq.com/embed/@fgregg/petitions-for-representation-certification-elections-fi@446?cells=cumulative"></iframe>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[Schemas for the data systems for the FMCS and NLRB case management systems]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">2022 Voluntary Recognitions</title>
      <link href="https://notes.labordata.bunkum.us/2023/02/07/voluntary-recognitions" rel="alternate" type="text/html" title="2022 Voluntary Recognitions" />
      <published>2023-02-07T00:00:00+00:00</published>
      <updated>2023-02-07T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2023/02/07/voluntary-recognitions</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2023/02/07/voluntary-recognitions"><![CDATA[<p>Just got my FOIA back for the voluntary recognitions known to the NLRB
for the <a href="https://www.muckrock.com/foi/united-states-of-america-10/voluntary-recognitions-october-1-2022-december-31-2022-138379/">fourth quarter of 2022</a>, so here’s what we can say about voluntary recognitions in 2022.</p>

<p>In 2022, there were <a href="https://labordata.bunkum.us/voluntary_recognitions-9901464?sql=select%0D%0A++cast%28%0D%0A++++strftime%28%27%25Y%27%2C+%22Date+VR+Request+Received%22%29+as+int%0D%0A++%29+year%2C%0D%0A++count%28%22VR+Case+Number%22%29%2C%0D%0A++sum%28%22Number+of+Employees%22%29+as+year%0D%0Afrom%0D%0A++voluntary_recognitions%0D%0Awhere%0D%0A++year+%3D+2022%0D%0Agroup+by%0D%0A++year%3B">209 voluntary recognitions</a> known to the NLRB covering at least
11,182 workers. The caveat of “known to the NLRB” is important because there is no
requirement that voluntary recognitions be reported to the NLRB, and we have no 
idea about what proportion of voluntary recognitions are ever reported to the NLRB.</p>

<p>That said, in 2022, there were about <a href="https://labordata.bunkum.us/nlrb-ca1e99a?sql=with+distinct_units+as+%28%0D%0A++select%0D%0A++++distinct+cast%28strftime%28%27%25Y%27%2C+date_closed%29+as+int%29+as+year%2C%0D%0A++++voting_unit_id%2C%0D%0A++++unit_size%0D%0A++from%0D%0A++++filing%0D%0A++++inner+join+voting_unit+using+%28case_number%29%0D%0A++++inner+join+election+using+%28voting_unit_id%29%0D%0A++++inner+join+election_result+using+%28election_id%29%0D%0A++where%0D%0A++++case_type+%3D+%27RC%27%0D%0A++++and+year+%3D+2022%0D%0A++++and+reason_closed+%3D+%27Certific.+of+Representative%27%0D%0A++++and+ballot_type+%3D+%27Single+Labor+Organization%27%0D%0A%29%0D%0Aselect%0D%0A++year%2C%0D%0A++count%28voting_unit_id%29%2C%0D%0A++sum%28unit_size%29%0D%0Afrom%0D%0A++distinct_units">1,100 NLRB
certifications</a>
that resulted in certified bargaining representative covering over
55,000 workers.</p>

<p>Understanding organizing activity outside the NLRB process remains a priority.</p>

<p>The future of this NLRB data is not certain as <a href="https://www.nlrb.gov/news-outreach/news-story/nlrb-issues-notice-of-proposed-rulemaking-on-fair-choice-and-employee">the NLRB is
considering returning to the previous voluntary recognition bar
rule</a>,
which prevents the filing of representation election petition within six
months of a voluntary recognition. In 2020, Trump’s NLRB modified the
rule to require a notice to the NLRB for the rule to take effect along
with a 45-day period after the receipt of notice when a petition could
be filed. The notification requirements is what has produced these
records, and it’s likely when the requirement is rescinded, the data
will cease.</p>

<h2 id="chips">CHIPS</h2>
<p>With the assistance of <a href="https://www.jpferguson.net/">JP Ferguson</a>,
we’ve figured out the <a href="https://github.com/labordata/CHIPS#erd-diagram">codes for many of the fields in
CHIPS</a>, the
1984-2000ish NLRB case management system.</p>

<p>It’s going to be pretty hard to do much more without more
documentation, than what’s <a href="https://catalog.archives.gov/id/627716">available from the National
Archive</a>. If you have any
CHIPS manuals from the late 1990s or early 2000s, please let me know.</p>

<h2 id="pre-chips">Pre-CHIPS</h2>
<p>Also, thanks to <a href="https://www.jpferguson.net/blog/blog-post-title-one-ez8bs-gbmmb">JP Ferguson’s
blog</a>,
I learned that there were a number of electronic records for <a href="https://archive.ciser.cornell.edu/studies/239">Unfair
Labor Cases</a>, <a href="https://archive.ciser.cornell.edu/studies/2103">Representation Cases</a>, and <a href="https://archive.ciser.cornell.edu/studies/237">Elections</a> on deposit at <a href="https://archive.ciser.cornell.edu/about">CISER</a>.</p>

<p>These records go back to the early 1960s and 1970s, which is fabulous.</p>

<p>They are also a bit of a mystery.</p>

<p>The FOIA officer at the NLRB, who is
very good and very knowledgeable about the agency’s information
systems knows nothing about these data, but the archivist at the
Cornell Center of Social Sciences confirms that these records came
from the NLRB. If you know anything more about these records, please
let me know.</p>

<p>I’m planning on adding these to the the warehouse.</p>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[Voluntary recognitions and older NLRB data]]></summary>
      

      
      
    </entry>
  
    <entry>
      

      <title type="html">Thirty-nine years of Unfair Labor Practice Case Data</title>
      <link href="https://notes.labordata.bunkum.us/2023/02/01/39-years-of-ulp" rel="alternate" type="text/html" title="Thirty-nine years of Unfair Labor Practice Case Data" />
      <published>2023-02-01T00:00:00+00:00</published>
      <updated>2023-02-01T00:00:00+00:00</updated>
      <id>https://notes.labordata.bunkum.us/2023/02/01/39-years-of-ulp</id>
      
      
        <content type="html" xml:base="https://notes.labordata.bunkum.us/2023/02/01/39-years-of-ulp"><![CDATA[<p>When my first child was born, I bought an <a href="https://www.massmadesoul.com/olivetti-praxis-48">Olivetti Praxis
48</a> to restore as I
stayed up with him.</p>

<p>Last month, we welcomed another child into our world, and the
parternal urge has been channeled towards refurbishing archives of the
two previous NLRB case management systems: the <a href="https://labordata.bunkum.us/cats">Case Activity Tracking
System</a> (CATS) that the NLRB used
from around 1999 through 2011 and the <a href="https://labordata.bunkum.us/chips">Case Handling Information
Processing System</a> (CHIPS)–the
first NLRB electronic system–that was in operation from 1984 through
2000.</p>

<p>I’ve had versions of these datasets on the labor data warehouse
for a while, but I’ve located and added the ULP cases to the CATS
data, and converted many columns in the CHIPS data to forms that are
much easier to use.</p>

<p>This means that we now have <a href="https://observablehq.com/@fgregg/nlrb-cases">pretty detailed case-level data for unfair
labor practice cases going back to 1984</a>. I think this is the first time
that this has all been pulled together. We also have the
representation cases for the same period, which is also fantastic, but
Hank Farber has had that data for a while.</p>

<p>One thing that the CHIPS data helps me see is how interesting the year
2000 is. In his wonderful article, <a href="https://www.nber.org/papers/w19908">“Union Organizing Decisions in a
Deteriorating Environment: The Composition of Representation Elections
and the Decline in Turnout”</a>, Hank Farber
points out the turn of the millenium as a transition point in how workers vote in NLRB elctions and what types of workplaces organizers bring to the NLRB.</p>

<p>In <a href="https://observablehq.com/@fgregg/improving-success-of-individual-certification-petitions">an analysis that covers some of the same
ground</a>,
I looked at the changing success rate of NLRB election petitions, and
2000 is the year when the win rate of elections, and petitions really
starts to increase.</p>

<p>Farber doesn’t really put forward an explanation about why 2000 was
such a pivotal year. His paper has a very nice argument about why we
could tend to see a higher NLRB win rate and fewer NLRB petitions
(basically, if it’s much more costly for unions to run NLRB election
campaigns, they will tend to focus on organzing worksites where they
have a high chance of winning). But Farber doesn’t identify anything
that was particularly important for making organizing harder in 2000.</p>

<p>I don’t really know either. Maybe you have ideas.</p>

<h3 id="help-with-the-unfair-labor-data">Help with the Unfair Labor Data</h3>
<p>The CHIPS data comes from records on <a href="https://catalog.archives.gov/id/627716">deposit with the National
Archive</a>. Unfortunately, their
deposit doesn’t have all the lookup tables that you would need to make sense
of the data. To figure out, say, which union is involved in a representation
certification case, we need to link a numeric ID to a table of union names.</p>

<p>The information we need for that is in <a href="https://s3.amazonaws.com/NARAprodstorage/opastorage/live/1/8902/890201/content/arcmedia/electronic-records/rg-025/chips/113.1CL.pdf">a big codebook</a>, but it needs to get
transcribed into tables. Let me know if that’s something you might want to help with.</p>

<h2 id="foia-corner">FOIA Corner</h2>
<p>Two interesting FOIAs came in recently:</p>

<ul>
  <li>In April 2022, the FMCS started offering card-check services. <a href="https://www.muckrock.com/foi/united-states-of-america-10/card-checks-april-2022-november-2022-137690/">From
April 2022 through November 2022</a>, FMCS has counted cards
for 51 groups of workers covering at least 1,544 workers.</li>
  <li>I got back the <a href="https://www.muckrock.com/foi/illinois-168/petitions-election-results-and-certification-of-representations-138559/">FOIA from the Illinois Labor Relations
Board</a>
that I discussed in <a href="/2023/01/06/state-foia">a previous
post</a>. It
was a great response and very heartening that this approach could
succeed.</li>
</ul>

<p>Working with the CATS and CHIPS databases, there’s a lot of data that
was recorded in those data systems that is not publicly exposed to on
the NLRB website. So, I also submitted <a href="https://www.muckrock.com/foi/united-states-of-america-10/schema-or-table-definitions-for-the-nxgen-case-management-system-139981/">a FOIA for the database schema
of NLRB’s current case management system NxGen</a>.</p>]]></content>
      

      
      
      
      
      

      <author>
          <name>Forest Gregg</name>
        
        
          <uri>https://bunkum.us/</uri>
        
      </author>

      
        
      

      

      
      
        <summary type="html"><![CDATA[A tour throough the archives.]]></summary>
      

      
      
    </entry>
  
</feed>
