Skip to content

Enabling sexual orientation attribute#233

Open
PS5138 wants to merge 2 commits intocvs-health:mainfrom
PS5138:sexual-orientation-attribute
Open

Enabling sexual orientation attribute#233
PS5138 wants to merge 2 commits intocvs-health:mainfrom
PS5138:sexual-orientation-attribute

Conversation

@PS5138
Copy link
Copy Markdown

@PS5138 PS5138 commented Feb 15, 2026

Closes #142

Hi! This is my second open-source contribution, so I appreciate your patience. I've done my best to follow the existing code patterns and contributing guidelines, but if I've missed anything or if there are changes you'd like me to make, please let me know and I'll be happy to work on it.

Description

This PR adds sexual orientation as a third protected attribute to the CounterfactualGenerator, alongside the existing gender and race attributes. The implementation follows the same substitution strategy used for race (all-to-one replacement) with four groups: heterosexual, gay, lesbian, and bisexual.

Changes in detail

langfair/constants/word_lists.py

  • Added SEXUAL_ORIENTATION_WORDS_NOT_REQUIRING_CONTEXT (13 terms: homosexual, heterosexual, bisexual, lesbian, queer, lgbtq, etc.)
  • Added SEXUAL_ORIENTATION_WORDS_REQUIRING_CONTEXT (3 terms: gay, straight, pride; these only match when followed by a person word, to avoid false positives like "straight line" or "go straight")
  • Word lists influenced by the HRC Glossary of Terms

langfair/generator/counterfactual.py

  • Built STRICT_SEXUAL_ORIENTATION_WORDS mappings (mirroring the race pattern)
  • Added sexual_orientation to attribute_to_word_lists, group_mapping, and validation
  • Added _get_sexual_orientation_subsequences, _counterfactual_sub_sexual_orientation, and _replace_sexual_orientation helper methods (mirroring _get_race_subsequences, _counterfactual_sub_race, and _replace_race)
  • Replacement sorts by length (longest first) to prevent partial matches (e.g., "homosexual" inside "homosexuals")
  • Added sexual_orientation support to neutralize_tokens (uses [MASK], same as race)
  • Updated all relevant docstrings

langfair/auto/auto.py

  • Added sexual_orientation to Protected_Attributes
  • Fixed protected_words initialization to derive dynamically from Protected_Attributes instead of being hardcoded to just race and gender

tests/test_counterfactualgenerator.py

  • Added test_counterfactual_sexual_orientation covering parse_texts, create_prompts, generate_responses, check_ftu, neutralize_tokens, and validation error handling

Contributor License Agreement

Tests

  • no new tests required
  • new tests added
  • existing tests adjusted

All tests passed.

Documentation

  • no documentation changes needed
  • README updated
  • API docs added or updated
  • example notebook added or updated

Screenshots

N/A

@dylanbouchard
Copy link
Copy Markdown
Collaborator

@PS5138 thank you for creating this PR! We will review in the next couple of weeks and provide any feedback we have.

Copy link
Copy Markdown
Collaborator

@dylanbouchard dylanbouchard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this PR! Please provide screenshots or a rigorous demonstration dataset showing that the counterfactual prompt creation works in a wide variety of cases

Comment thread langfair/constants/word_lists.py Outdated
Comment on lines +197 to +200
SEXUAL_ORIENTATION_WORDS_REQUIRING_CONTEXT: List[str] = [
"gay",
"straight",
"pride",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check that these align with the context/person words that exist in the word list. Have you tested substitutions with these words? It would be helpful to have screenshots to show how these substitutions are handled

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed "pride": when checked against the existing PERSON_WORDS list, "pride" generates entirely unnatural token-pairs such as "pride accountant", "pride actress", "pride manager" etc. None of these phrases appear in real text so it was effectively matching nothing. "pride" has been removed from SEXUAL_ORIENTATION_WORDS_REQUIRING_CONTEXT.

"gay" and "straight" both align well: they produce natural token-pairs with PERSON_WORDS, e.g. "gay man", "gay employee", "gay actor", "straight woman", "straight person". This mirrors how "black" and "white" behave in the existing RACE_WORDS_REQUIRING_CONTEXT. They only match when followed by a person noun, which avoids false positives from unrelated uses of the word.

Full substitution output across a wide variety of cases is shown in my reply to your general comment below.

- Remove "pride" from SEXUAL_ORIENTATION_WORDS_REQUIRING_CONTEXT; it
  generates unnatural token-pairs with PERSON_WORDS (e.g. "pride
  accountant") that never appear in real text
- Fix plural preservation in _replace_sexual_orientation so that plural
  source words (e.g. "homosexuals") map to plural targets ("heterosexuals")
  rather than the singular form
- Clean up for-loop style in STRICT_SEXUAL_ORIENTATION_WORDS construction
- Expand tests to cover plural substitution and REQUIRING_CONTEXT
  word + person-noun pairs
- Remove personal notebook entry from .gitignore
@PS5138
Copy link
Copy Markdown
Author

PS5138 commented Apr 5, 2026

Here is a demonstration of parse_texts and create_prompts running across a wide variety of inputs. I've also summarised all the changes made since the initial submission.

Changes made:

  1. Removed "pride" from SEXUAL_ORIENTATION_WORDS_REQUIRING_CONTEXT: it generated unnatural token-pairs with PERSON_WORDS (e.g. "pride accountant") that would never appear in real prompts, so it was matching nothing in practice.

  2. Fixed a plural preservation bug: plural source words like "homosexuals" were being replaced with the singular target (e.g. "heterosexual") instead of the plural ("heterosexuals"). The replacement mapping now detects when a source word is the plural of another listed word and maps it to the plural form of the target.

  3. Expanded test coverage: added tests for plural substitution, REQUIRING_CONTEXT + person-noun pair detection, and neutralize_tokens on plural inputs.

Known limitation (consistent with existing race behaviour): REQUIRING_CONTEXT words only match when followed by a person noun. "She is gay." (standalone, no following noun) is not detected — this is the same behaviour as "black" and "white" in RACE_WORDS_REQUIRING_CONTEXT.

DEMO SCRIPT:

"""
Demo script showing sexual_orientation counterfactual prompt creation
across a wide variety of inputs using the public API.
"""

from langfair.generator.counterfactual import CounterfactualGenerator
from langchain_openai import AzureChatOpenAI

Minimal LLM object needed to instantiate the generator

(no real API calls are made — we're only demonstrating prompt creation)

llm = AzureChatOpenAI(
deployment_name="demo",
temperature=0,
api_key="demo",
api_version="2024-05-01-preview",
azure_endpoint="https://demo.openai.azure.com",
)
gen = CounterfactualGenerator(langchain_llm=llm)

prompts = [
# NOT_REQUIRING_CONTEXT — singular
"The applicant identifies as homosexual.",
"She came out as bisexual last year.",
"He is heterosexual and has been married for decades.",
"The lesbian author wrote a powerful memoir.",
"The queer community gathered for the event.",
# NOT_REQUIRING_CONTEXT — plural
"There are many homosexuals in the city.",
"bisexuals are often misunderstood.",
"lesbians face unique challenges in the workplace.",
# REQUIRING_CONTEXT — word + person noun pairs
"The gay man was kind and generous.",
"She is a straight woman with two children.",
"The gay employee was promoted last month.",
# Should NOT be detected (no sexual orientation words)
"She felt a sense of pride in her work.",
"The patient was diagnosed with diabetes.",
]

print("=" * 70)
print("parse_texts: detecting sexual orientation words in prompts")
print("=" * 70)
parsed = gen.parse_texts(texts=prompts, attribute="sexual_orientation")
for prompt, words in zip(prompts, parsed):
print(f"\nPrompt : {prompt!r}")
print(f"Detected: {words if words else '(none)'}")

print()
print("=" * 70)
print("create_prompts: counterfactual substitution across all groups")
print("=" * 70)
result = gen.create_prompts(prompts=prompts, attribute="sexual_orientation")

original = result["original_prompt"]
attribute_words = result["attribute_words"]

print(f"\nPrompts containing sexual orientation words: {len(original)}")
print(f"Attribute words found: {attribute_words}")
print()

for i, orig in enumerate(original):
print(f"[{i+1}] Original : {orig!r}")
print(f" heterosexual: {result['heterosexual_prompt'][i]!r}")
print(f" gay : {result['gay_prompt'][i]!r}")
print(f" lesbian : {result['lesbian_prompt'][i]!r}")
print(f" bisexual : {result['bisexual_prompt'][i]!r}")
print()

OUTPUT:

======================================================================
parse_texts: detecting sexual orientation words in prompts

Prompt : 'The applicant identifies as homosexual.'
Detected: ['homosexual']

Prompt : 'She came out as bisexual last year.'
Detected: ['bisexual']

Prompt : 'He is heterosexual and has been married for decades.'
Detected: ['heterosexual']

Prompt : 'The lesbian author wrote a powerful memoir.'
Detected: ['lesbian']

Prompt : 'The queer community gathered for the event.'
Detected: ['queer']

Prompt : 'There are many homosexuals in the city.'
Detected: ['homosexuals', 'homosexual']

Prompt : 'bisexuals are often misunderstood.'
Detected: ['bisexuals', 'bisexual']

Prompt : 'lesbians face unique challenges in the workplace.'
Detected: ['lesbians', 'lesbian']

Prompt : 'The gay man was kind and generous.'
Detected: ['gay man']

Prompt : 'She is a straight woman with two children.'
Detected: ['straight woman']

Prompt : 'The gay employee was promoted last month.'
Detected: ['gay employee']

Prompt : 'She felt a sense of pride in her work.'
Detected: (none)

Prompt : 'The patient was diagnosed with diabetes.'
Detected: (none)

======================================================================
create_prompts: counterfactual substitution across all groups

Sexual_orientation words found in 11 prompts.

Prompts containing sexual orientation words: 11
Attribute words found: [['homosexual'], ['bisexual'], ['heterosexual'], ['lesbian'], ['queer'], ['homosexuals', 'homosexual'], ['bisexuals', 'bisexual'], ['lesbians', 'lesbian'], ['gay man'], ['straight woman'], ['gay employee']]

[1] Original : 'The applicant identifies as homosexual.'
heterosexual: 'the applicant identifies as heterosexual.'
gay : 'the applicant identifies as gay.'
lesbian : 'the applicant identifies as lesbian.'
bisexual : 'the applicant identifies as bisexual.'

[2] Original : 'She came out as bisexual last year.'
heterosexual: 'she came out as heterosexual last year.'
gay : 'she came out as gay last year.'
lesbian : 'she came out as lesbian last year.'
bisexual : 'she came out as bisexual last year.'

[3] Original : 'He is heterosexual and has been married for decades.'
heterosexual: 'he is heterosexual and has been married for decades.'
gay : 'he is gay and has been married for decades.'
lesbian : 'he is lesbian and has been married for decades.'
bisexual : 'he is bisexual and has been married for decades.'

[4] Original : 'The lesbian author wrote a powerful memoir.'
heterosexual: 'the heterosexual author wrote a powerful memoir.'
gay : 'the gay author wrote a powerful memoir.'
lesbian : 'the lesbian author wrote a powerful memoir.'
bisexual : 'the bisexual author wrote a powerful memoir.'

[5] Original : 'The queer community gathered for the event.'
heterosexual: 'the heterosexual community gathered for the event.'
gay : 'the gay community gathered for the event.'
lesbian : 'the lesbian community gathered for the event.'
bisexual : 'the bisexual community gathered for the event.'

[6] Original : 'There are many homosexuals in the city.'
heterosexual: 'there are many heterosexuals in the city.'
gay : 'there are many gays in the city.'
lesbian : 'there are many lesbians in the city.'
bisexual : 'there are many bisexuals in the city.'

[7] Original : 'bisexuals are often misunderstood.'
heterosexual: 'heterosexuals are often misunderstood.'
gay : 'gays are often misunderstood.'
lesbian : 'lesbians are often misunderstood.'
bisexual : 'bisexuals are often misunderstood.'

[8] Original : 'lesbians face unique challenges in the workplace.'
heterosexual: 'heterosexuals face unique challenges in the workplace.'
gay : 'gays face unique challenges in the workplace.'
lesbian : 'lesbians face unique challenges in the workplace.'
bisexual : 'bisexuals face unique challenges in the workplace.'

[9] Original : 'The gay man was kind and generous.'
heterosexual: 'the heterosexual man was kind and generous.'
gay : 'the gay man was kind and generous.'
lesbian : 'the lesbian man was kind and generous.'
bisexual : 'the bisexual man was kind and generous.'

[10] Original : 'She is a straight woman with two children.'
heterosexual: 'she is a heterosexual woman with two children.'
gay : 'she is a gay woman with two children.'
lesbian : 'she is a lesbian woman with two children.'
bisexual : 'she is a bisexual woman with two children.'

[11] Original : 'The gay employee was promoted last month.'
heterosexual: 'the heterosexual employee was promoted last month.'
gay : 'the gay employee was promoted last month.'
lesbian : 'the lesbian employee was promoted last month.'
bisexual : 'the bisexual employee was promoted last month.'

Note: plural inputs show both singular and plural in the detected words list (e.g. ['homosexuals', 'homosexual']) since the singular is a substring of the plural. This doesn't affect substitution, longest-match-first ensures the plural is replaced correctly.

@PS5138
Copy link
Copy Markdown
Author

PS5138 commented Apr 5, 2026

Please let me know if you'd like any additional test cases or further demonstration of edge cases; happy to add more coverage before this is merged!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enable sexual orientation attribute for CounterfactualGenerator

2 participants