Skip to main content

Word-embedding seed expansion and document scoring. Bring your own seeds. Originally Li, Mai, Shen, Yan (2021, RFS).

Project description

lmsy_w2v_rfs: Word2Vec dictionary expansion and document scoring

Open in Colab PyPI version License: MIT

lmsy_w2v_rfs implements the Word2Vec seed-expansion method for document scoring introduced in Li, Mai, Shen, and Yan (2021). A researcher specifies a small set of seed words for each concept to be measured; the package trains Word2Vec on the target corpus, expands each concept's seeds into a corpus-specific dictionary of related words and multi-word phrases, and produces document-level scores by TF-IDF–weighted dictionary matching.

Citation

If you find the package useful, please cite the paper the method is based on:

Li, Kai, Feng Mai, Rui Shen, and Xinyan Yan (2021), "Measuring Corporate Culture Using Machine Learning," Review of Financial Studies 34(7):3265–3315, doi.org/10.1093/rfs/hhaa079.

BibTeX
@article{li2021measuring,
  title={Measuring Corporate Culture Using Machine Learning},
  author={Li, Kai and Mai, Feng and Shen, Rui and Yan, Xinyan},
  journal={The Review of Financial Studies},
  volume={34}, number={7}, pages={3265--3315}, year={2021},
  doi={10.1093/rfs/hhaa079}
}

This package is a general update of the paper's method. The original code for the paper is at MS20190155/Measuring-Corporate-Culture-Using-Machine-Learning.


Install

pip install -U lmsy_w2v_rfs

The base install runs out of the box with preprocessor="none" (whitespace tokenization). For richer Phase 1 parsing (lemmatization, named-entity masking, and dependency-based multi-word expressions) install an optional backend:

pip install -U "lmsy_w2v_rfs[spacy]" && python -m spacy download en_core_web_sm

For reproduction of the 2021 paper, use the CoreNLP backend (slower; needs Java and a one-time ~1 GB download):

pip install -U "lmsy_w2v_rfs[corenlp]"
lmsy-w2v-rfs download-corenlp

Quickstart

Open in Colab   Try it now, no install, runs on a bundled 2,000-review demo corpus.

Researchers usually start from a table of documents. Point the pipeline at a CSV, declare a few seed words per concept, and run:

from lmsy_w2v_rfs import Pipeline, Config

seeds = {
    "innovation":   ["innovation", "innovative", "creativity", "creative"],
    "teamwork":     ["teamwork", "collaboration", "collaborate", "supportive"],
    "compensation": ["pay", "salary", "compensation", "benefits", "bonus"],
}

p = Pipeline.from_csv(
    "reviews.csv", text_col="text", id_col="review_id",
    work_dir="runs/quickstart",
    config=Config(seeds=seeds),     # preprocessor="none" by default
)
p.run()                     # phrase + train + expand + score
p.show_dictionary(top_k=10) # inspect the expanded dictionary
print(p.score_df("TFIDF"))  # per-document scores

The first two concepts come from the paper's culture construct; compensation is a concept outside the five culture dimensions, included to show the method generalizes. On a corpus of employee reviews, the expansion fills each concept with the corpus's own vocabulary — note that the seeds never mentioned 401k_match, dental, or mentorship:

=== innovation ===
  seeds:    innovation, innovative, creativity, creative
  expanded: creativity, entrepreneurial, passion, open_communication, fostering
=== teamwork ===
  seeds:    teamwork, collaboration, collaborate, supportive
  expanded: collaboration, inclusion, mutual_respect, caring, fosters
=== compensation ===
  seeds:    pay, salary, compensation, benefits, bonus
  expanded: salary, competitive, 401k_match, dental, medical, bonuses

Other ways to load documents and seeds

The same Pipeline also accepts in-memory lists, DataFrames, JSONL, and directories:

Pipeline(texts=[...], doc_ids=[...], work_dir=..., config=cfg)              # in-memory list
Pipeline.from_csv("docs.csv", text_col="text", id_col="id", ...)            # CSV
Pipeline.from_dataframe(df, text_col="text", id_col="id", ...)              # DataFrame
Pipeline.from_directory("./docs/", pattern="*.txt", ...)                    # one file per doc
Pipeline.from_text_file("docs.txt", id_path="ids.txt", ...)                 # one doc per line
Pipeline.from_jsonl("docs.jsonl", text_key="text", id_key="id", ...)        # JSONL

Seeds accept a Python dict, a JSON file, or a plain text file:

from lmsy_w2v_rfs import load_seeds
Config(seeds=load_seeds("my_seeds.json"))     # or .txt, or pass a dict directly

CLI: lmsy-w2v-rfs run --seeds my_seeds.txt --input docs.csv --input-format csv --out runs/x.

Reproducing Li et al. (2021)

The package ships the paper's 47 seed words across five culture dimensions, and the CoreNLP backend reproduces the paper's Phase 1 parsing:

from lmsy_w2v_rfs import Pipeline, Config, load_example_seeds

seeds = load_example_seeds("culture_2021")    # 47 seeds, 5 dimensions
config = Config(seeds=seeds, preprocessor="corenlp")  # needs Java; see Install

The construction procedure

The package implements the four-step construction procedure of Li et al. (2021). Each step is a method on Pipeline; calling .run() executes them in order and saves intermediate artifacts under work_dir/ so any step can be redone without redoing the others.

Step 1: Two-step phrase construction

Phrases carry meaning that single words cannot. The package extracts them in two complementary steps targeting different kinds of phrases.

Step 1a, parser-based (general-English phrases). A dependency parser identifies fixed multiword expressions (with_respect_to, rather_than) and compound words (intellectual_property, healthcare_provider). The parser also lemmatizes (stocksstock) and masks named entities as [NER:ORG] placeholders so proper nouns do not bias the vector space. The 121-token SRAF generic stopword list is removed in the cleaning pass that follows.

Config(preprocessor=...) Backend Needs
"none" (default) whitespace tokenize, lowercase only base install
"static" NLTK MWETokenizer over a curated list base install
"spacy" spaCy (lemmas, NER, dependency MWEs) [spacy] extra + a model
"corenlp" (paper-faithful) Stanford CoreNLP via stanza.server [corenlp] extra + Java
"stanza" stanza Pipeline [stanza] extra

Step 1b, statistical (corpus-specific phrases). After Step 1a, gensim's Phrases scans the parsed corpus for statistically significant adjacent-token co-occurrences and joins them with _. A second pass over the bigram-joined corpus learns trigrams. This step identifies recurring collocations specific to the corpus: an earnings-call corpus surfaces forward_looking_statement and cost_of_capital; a product-review corpus surfaces customer_service and delivery_time; a Glassdoor corpus surfaces work_life_balance and growth_opportunity.

from lmsy_w2v_rfs import Config, load_example_seeds

seeds = load_example_seeds("culture_2021")  # or any dict[str, list[str]]
Config(
    seeds=seeds,
    use_gensim_phrases=True,
    phrase_passes=2,            # 1 = bigrams; 2 = bigrams + trigrams
    phrase_min_count=10,        # works on a ~270k-doc corpus
    phrase_threshold=10.0,      # for smaller corpora try 3 / 5.0
)

The phrase-tagged corpus is written to work_dir/corpora/pass2.txt and can be opened directly to inspect the joined phrases.

Step 2: Word2Vec

Pipeline.train() fits a gensim.models.Word2Vec on the phrase-tagged corpus. Every word and phrase receives a 300-dimensional vector. Defaults match the 2021 paper:

from lmsy_w2v_rfs import Config, load_example_seeds

seeds = load_example_seeds("culture_2021")  # or any dict[str, list[str]]
Config(seeds=seeds, w2v_dim=300, w2v_window=5, w2v_min_count=5, w2v_epochs=20)

The model is saved at work_dir/models/w2v.mod and is available as p.w2v for ad-hoc queries.

Step 3: Seed expansion

Pipeline.expand_dictionary() builds the per-concept dictionary by:

  1. Averaging the in-vocabulary seed vectors for the concept.
  2. Taking the top n_words_dim (default 500) tokens by cosine similarity to that mean.
  3. Resolving cross-loadings: a token close to multiple concepts is assigned to the one whose seed mean it is closest to.
  4. Dropping [NER:*] placeholders so named entities never enter the dictionary.

The result is written to work_dir/outputs/expanded_dict.csv, one column per concept, sorted by descending similarity to the seed mean.

p.show_dictionary(top_k=10)         # prints per-concept seeds + top expansions
p.dictionary_preview(top_k=10)      # DataFrame for notebook display

Step 4: Manual dictionary inspection

Nearest-neighbor expansion surfaces noise: off-topic terms, industry-specific outliers, words too general to be informative. Two ways to remove them, both atomic across the in-memory dictionary and the on-disk CSV:

# Programmatic, replicable in a notebook:
p.edit_dictionary(
    remove={"innovation": ["fantastic", "incredible"]},
    add={"innovation": ["patent"]},
)

# Spreadsheet-driven, faster on a big dictionary:
#   1. open p.dict_path in Excel or any text editor
#   2. edit, save
#   3. p.reload_dictionary()

Cached scores are dropped after curation. Call p.score() to rescore against the curated dictionary.


Scoring

A document's score on a concept is the sum of TF-IDF weights for every dictionary token present in the document, divided by total document length.

Method Weight per dictionary hit Source
TFIDF tf · log(N/df) 2021 paper (the published measure)
TF tf alternative
WFIDF (1 + log tf) · log(N/df) alternative (sublinear tf)
TFIDF+SIMWEIGHT, WFIDF+SIMWEIGHT × 1/ln(2 + rank) rank-weighted variant

The +SIMWEIGHT variants additionally weight each word by its rank in the similarity-ordered dictionary (1/ln(2 + rank)), so words nearer the seed centroid count more and peripheral expansion words count less. This rank-based similarity weighting is the scheme several studies building on the method have adopted. It is rank-based, not a function of the raw cosine-similarity value.

p.score(methods=("TFIDF",))
p.score_df("TFIDF")

Outputs land at work_dir/outputs/scores_<METHOD>.csv.

Which words drive a dimension?

To validate dictionary quality, decompose each dimension's score into the contribution of each dictionary word across the corpus:

contrib = p.word_contributions("TFIDF")   # dimension, word, contribution, relative, cumulative

This writes work_dir/outputs/word_contributions_<METHOD>.csv and shows, per dimension, each word's share and the running cumulative share — the standard way to check that (say) innovation is driven by genuine innovation terms rather than a few high-IDF artifacts.


Large corpora

Once parsing finishes, downstream stages stream through disk: clean reads parsed sentences line by line; phrase and train use gensim's PathLineSentences so the training corpus is never fully materialized. The bottleneck is the input stage: the document loader holds the corpus in a Python list before parsing begins.

For corpora beyond a few hundred thousand documents, or when running on a cluster, see the Run on HPC how-to for the multi-shard workflow, SLURM and SGE templates, and BLAS thread-cap instructions.


Configuration parameters

Config(
    seeds=...,                         # required: dict[str, list[str]]

    # Step 1a
    preprocessor="none",               # "none" | "static" | "spacy" | "corenlp" | "stanza"
    mwe_list=None,                     # None | "finance" | path to a curated list
    spacy_model="en_core_web_sm",
    parse_chunk_size=0,                # >0 processes docs in batches (caps memory on big corpora)
    n_cores=4,
    corenlp_memory="6G",
    corenlp_port=9002,
    corenlp_timeout_ms=120_000,        # per-request CoreNLP timeout (ms)
    corenlp_max_char_length=1_000_000, # raise for very long transcripts
    corenlp_properties={},             # extra CoreNLP server properties (override/add)

    # Step 1b
    use_gensim_phrases=True,
    phrase_passes=2,
    phrase_threshold=10.0,
    phrase_min_count=10,
    phrase_extra={},                   # extra kwargs -> gensim Phrases (e.g. {"scoring": "npmi"})

    # Step 2
    w2v_dim=300,
    w2v_window=5,
    w2v_min_count=5,
    w2v_epochs=20,
    w2v_sg=0,                          # 0 = CBOW (matches the original); 1 = skip-gram
    w2v_extra={},                      # extra kwargs -> gensim Word2Vec (e.g. {"negative": 10, "hs": 0})

    # Step 3
    n_words_dim=500,                   # paper's threshold for the dictionary cutoff
    dict_restrict_vocab=None,
    min_similarity=0.0,

    # Scoring (extensions beyond the 2021 paper)
    tfidf_normalize=False,
    zca_whiten=False,                  # ZCA-decorrelate the concept columns; see docs/how-to/whiten-scores.md
    zca_epsilon=1e-6,

    random_state=42,
)

Resources

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

lmsy_w2v_rfs-0.1.5.tar.gz (62.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

lmsy_w2v_rfs-0.1.5-py3-none-any.whl (56.2 kB view details)

Uploaded Python 3

File details

Details for the file lmsy_w2v_rfs-0.1.5.tar.gz.

File metadata

  • Download URL: lmsy_w2v_rfs-0.1.5.tar.gz
  • Upload date:
  • Size: 62.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for lmsy_w2v_rfs-0.1.5.tar.gz
Algorithm Hash digest
SHA256 dfbc1fe51546a2cdce194d1108fd6551c1554bc5d6fcb43736537a684669ec31
MD5 3eb9668a1e3013d25da37a198b45894f
BLAKE2b-256 7cec0a9a8b886fc3cfea03908adea0272b03831669ec9cd28b2d6cd615e9f953

See more details on using hashes here.

File details

Details for the file lmsy_w2v_rfs-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: lmsy_w2v_rfs-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 56.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for lmsy_w2v_rfs-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 8270ae2e0f73037e02cbb943ab812a76128ba36615b566aaf41922b0f827089d
MD5 3ceff11b5ae301e05286d9d1143b1a02
BLAKE2b-256 a7face0c69c418520c977c35c2c844efbb7bf3889c0f49be8fa20b230a1e231d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page