From a809d00d4ae6233cb38cd18e34191c5eaf3bf815 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 1 Jun 2026 12:14:04 +0000 Subject: [PATCH 1/3] Add "only bots" aggregation option to aggregation explorer Adds a third state to the bot toggle in the aggregation explorer, turning it into a select with "Exclude Bots" / "Include Bots" / "Only Bots". The new option filters aggregation inputs to only bot-authored forecasts. --- front_end/messages/en.json | 3 +++ .../components/aggregation_graph_panel.tsx | 1 + .../aggregation_method_selector.tsx | 25 +++++++++++++++---- .../hooks/aggregation-data.ts | 21 +++++++++++----- .../hooks/explorer-state.ts | 5 +++- .../aggregation-explorer/hooks/query-state.ts | 4 ++- .../app/(main)/aggregation-explorer/types.ts | 1 + .../aggregation_explorer.shared.ts | 2 ++ utils/serializers.py | 10 ++++++-- utils/the_math/aggregations.py | 14 ++++++++--- utils/views.py | 4 +++ 11 files changed, 71 insertions(+), 19 deletions(-) diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 9cca3e7662..ea6af01b84 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -616,6 +616,9 @@ "questionWeight": "Question Weight", "questionWeightTooltip": "This question has a weight of {weight}%, providing {count, plural, =1 {{weightDiff}% fewer points} other {a {weightDiff}% point boost}} compared to regular Metaculus questions.", "includeBots": "Include Bots", + "excludeBots": "Exclude Bots", + "onlyBots": "Only Bots", + "bots": "Bots", "includeBotsTooltip": "For this question, bot forecasts contribute to the Community Prediction. Learn more about AI Benchmarking", "includeBotsInAggregatesLabel": "Include Bots in Aggregates", "includeBotsInAggregatesExplanation": "When enabled, bot forecasts will be included in aggregate calculations for this question and will affect scoring. Otherwise, they will affect neither.", diff --git a/front_end/src/app/(main)/aggregation-explorer/components/aggregation_graph_panel.tsx b/front_end/src/app/(main)/aggregation-explorer/components/aggregation_graph_panel.tsx index aa747cc5b7..319a747187 100644 --- a/front_end/src/app/(main)/aggregation-explorer/components/aggregation_graph_panel.tsx +++ b/front_end/src/app/(main)/aggregation-explorer/components/aggregation_graph_panel.tsx @@ -90,6 +90,7 @@ export default function AggregationGraphPanel({ choice: method.id, label: method.label, includeBots: method.includeBots, + onlyBots: method.onlyBots, color: colorById.get(method.id) ?? METAC_COLORS.gray["400"], })), [methods, colorById] diff --git a/front_end/src/app/(main)/aggregation-explorer/components/aggregation_method_selector.tsx b/front_end/src/app/(main)/aggregation-explorer/components/aggregation_method_selector.tsx index 8573e58307..7a0a9bc7f5 100644 --- a/front_end/src/app/(main)/aggregation-explorer/components/aggregation_method_selector.tsx +++ b/front_end/src/app/(main)/aggregation-explorer/components/aggregation_method_selector.tsx @@ -15,6 +15,8 @@ import LoadingIndicator from "@/components/ui/loading_indicator"; import Switch from "@/components/ui/switch"; import { useAuth } from "@/contexts/auth_context"; +type BotMode = "exclude" | "include" | "only"; + import AggregationLabel from "./aggregation_label"; import { AGGREGATION_EXPLORER_OPTIONS, @@ -99,6 +101,7 @@ type Props = { joinedBeforeDate?: string; userIds?: number[]; includeBots?: boolean; + onlyBots?: boolean; }) => void; defaultIncludeBots?: boolean; }; @@ -120,7 +123,9 @@ export default function AggregationMethodSelector({ AggregationMethod.recency_weighted ); const [selectedChildId, setSelectedChildId] = useState(null); - const [includeBots, setIncludeBots] = useState(defaultIncludeBots); + const [botMode, setBotMode] = useState( + defaultIncludeBots ? "include" : "exclude" + ); const [joinedBeforeDate, setJoinedBeforeDate] = useState(""); const [userFilterEnabled, setUserFilterEnabled] = useState(false); const [userIdsText, setUserIdsText] = useState(""); @@ -224,11 +229,18 @@ export default function AggregationMethodSelector({ ) : null} {showBotToggle ? ( -
+
- + setBotMode(e.target.value as BotMode)} + > + + + +
) : null} @@ -269,7 +281,10 @@ export default function AggregationMethodSelector({ parsedUserIds.length ? parsedUserIds : undefined, - includeBots: showBotToggle ? includeBots : undefined, + includeBots: + showBotToggle && botMode === "include" ? true : undefined, + onlyBots: + showBotToggle && botMode === "only" ? true : undefined, }); }} > diff --git a/front_end/src/app/(main)/aggregation-explorer/hooks/aggregation-data.ts b/front_end/src/app/(main)/aggregation-explorer/hooks/aggregation-data.ts index 395a199479..e4b2442775 100644 --- a/front_end/src/app/(main)/aggregation-explorer/hooks/aggregation-data.ts +++ b/front_end/src/app/(main)/aggregation-explorer/hooks/aggregation-data.ts @@ -26,10 +26,12 @@ export function buildConfigId( optionId: string, includeBots: boolean, joinedBeforeDate?: string, - userIds?: number[] + userIds?: number[], + onlyBots?: boolean ): string { const parts = [optionId]; - if (includeBots) parts.push("bots"); + if (onlyBots) parts.push("obots"); + else if (includeBots) parts.push("bots"); if (joinedBeforeDate) parts.push(joinedBeforeDate); if (userIds?.length) parts.push(`u${[...userIds].sort((a, b) => a - b).join(",")}`); @@ -42,6 +44,7 @@ export type SelectedAggregationConfig = { joinedBeforeDate?: string; userIds?: number[]; includeBots?: boolean; + onlyBots?: boolean; enabled?: boolean; }; @@ -52,6 +55,7 @@ export type AggregationQueryResult = { chips: string[]; // filter chips derived from config (for display in label component) method: string; // option.id, used for API response lookup includeBots: boolean; + onlyBots: boolean; joinedBeforeDate?: string; userIds?: number[]; isPending: boolean; @@ -106,7 +110,7 @@ export function buildDisplayLabel( option: AggregationOption, config: Pick< SelectedAggregationConfig, - "joinedBeforeDate" | "userIds" | "includeBots" | "optionId" + "joinedBeforeDate" | "userIds" | "includeBots" | "onlyBots" | "optionId" > ): string { let label = @@ -115,7 +119,9 @@ export function buildDisplayLabel( ? t("usersJoinedBefore", { date: config.joinedBeforeDate }) : t(option.labelKey as Parameters[0]); - if (option.supportsBotToggle && config.includeBots) { + if (option.supportsBotToggle && config.onlyBots) { + label += ` (${t("onlyBots")})`; + } else if (option.supportsBotToggle && config.includeBots) { label += ` (${t("withBots")})`; } @@ -140,13 +146,14 @@ export function buildChips( t: TranslateFunction, config: Pick< SelectedAggregationConfig, - "joinedBeforeDate" | "userIds" | "includeBots" + "joinedBeforeDate" | "userIds" | "includeBots" | "onlyBots" > ): string[] { const chips: string[] = []; if (config.joinedBeforeDate) chips.push(t("beforeDate", { date: config.joinedBeforeDate })); - if (config.includeBots) chips.push(t("withBots")); + if (config.onlyBots) chips.push(t("onlyBots")); + else if (config.includeBots) chips.push(t("withBots")); if (config.userIds?.length) chips.push(t("usersFilterLabel", { ids: config.userIds.join(", ") })); return chips; @@ -177,6 +184,7 @@ export function useAggregationData({ postId, questionId, includeBots: config.includeBots, + onlyBots: config.onlyBots, aggregationMethods: option.id, joinedBeforeDate: config.joinedBeforeDate, userIds: config.userIds, @@ -203,6 +211,7 @@ export function useAggregationData({ chips: buildChips(t, config), method: option.id, includeBots: !!config.includeBots, + onlyBots: !!config.onlyBots, joinedBeforeDate: config.joinedBeforeDate, userIds: config.userIds, isPending: query.isPending, diff --git a/front_end/src/app/(main)/aggregation-explorer/hooks/explorer-state.ts b/front_end/src/app/(main)/aggregation-explorer/hooks/explorer-state.ts index 1b8ee24fa9..0ad86734f5 100644 --- a/front_end/src/app/(main)/aggregation-explorer/hooks/explorer-state.ts +++ b/front_end/src/app/(main)/aggregation-explorer/hooks/explorer-state.ts @@ -145,12 +145,14 @@ export function useExplorerState(postData: PostWithForecasts) { joinedBeforeDate?: string; userIds?: number[]; includeBots?: boolean; + onlyBots?: boolean; }) => { const configId = buildConfigId( payload.optionId, payload.includeBots ?? false, payload.joinedBeforeDate, - payload.userIds + payload.userIds, + payload.onlyBots ); void setSelectedConfigs((prev) => { if (prev.some((c) => c.id === configId)) return prev; @@ -162,6 +164,7 @@ export function useExplorerState(postData: PostWithForecasts) { joinedBeforeDate: payload.joinedBeforeDate, userIds: payload.userIds, includeBots: payload.includeBots, + onlyBots: payload.onlyBots, enabled: true, }, ]; diff --git a/front_end/src/app/(main)/aggregation-explorer/hooks/query-state.ts b/front_end/src/app/(main)/aggregation-explorer/hooks/query-state.ts index 4926872a85..c1c15b7cb4 100644 --- a/front_end/src/app/(main)/aggregation-explorer/hooks/query-state.ts +++ b/front_end/src/app/(main)/aggregation-explorer/hooks/query-state.ts @@ -56,7 +56,8 @@ function parseConfigSpec(spec: string): SelectedAggregationConfig | null { const optionId = parts[0]; if (!optionId || !VALID_AGGREGATION_METHODS.has(optionId)) return null; - const includeBots = parts.includes("bots"); + const onlyBots = parts.includes("obots"); + const includeBots = !onlyBots && parts.includes("bots"); const joinedBeforeDate = parts.find((p) => /^\d{4}-\d{2}-\d{2}$/.test(p)); const userIdsStr = parts.find((p) => /^u\d/.test(p)); const userIds = userIdsStr @@ -71,6 +72,7 @@ function parseConfigSpec(spec: string): SelectedAggregationConfig | null { id, optionId: optionId as AggregationMethod, includeBots: includeBots || undefined, + onlyBots: onlyBots || undefined, joinedBeforeDate, userIds: userIds?.length ? userIds : undefined, enabled: disabled ? false : undefined, diff --git a/front_end/src/app/(main)/aggregation-explorer/types.ts b/front_end/src/app/(main)/aggregation-explorer/types.ts index 80fe62f116..72f41cdc80 100644 --- a/front_end/src/app/(main)/aggregation-explorer/types.ts +++ b/front_end/src/app/(main)/aggregation-explorer/types.ts @@ -68,5 +68,6 @@ export type AggregationTooltip = { choice: string; label: string; includeBots: boolean; + onlyBots: boolean; color: ThemeColor; }; diff --git a/front_end/src/services/api/aggregation_explorer/aggregation_explorer.shared.ts b/front_end/src/services/api/aggregation_explorer/aggregation_explorer.shared.ts index 690083a39b..bfd26bc23c 100644 --- a/front_end/src/services/api/aggregation_explorer/aggregation_explorer.shared.ts +++ b/front_end/src/services/api/aggregation_explorer/aggregation_explorer.shared.ts @@ -6,6 +6,7 @@ type AggregationExplorerParams = { postId?: number | string | null; questionId?: number | string | null; includeBots?: boolean; + onlyBots?: boolean; aggregationMethods?: string; userIds?: number[]; joinedBeforeDate?: string; @@ -17,6 +18,7 @@ class AggregationExplorerApi extends ApiService { post_id: params.postId?.toString() || "", question_id: params.questionId?.toString() || "", include_bots: params.includeBots?.toString() || "false", + only_bots: params.onlyBots?.toString() || "false", aggregation_methods: params.aggregationMethods || "", joined_before_date: params.joinedBeforeDate || "", ...(params.userIds !== undefined ? { user_ids: params.userIds } : {}), diff --git a/utils/serializers.py b/utils/serializers.py index 1608d766df..3169489033 100644 --- a/utils/serializers.py +++ b/utils/serializers.py @@ -84,6 +84,7 @@ class DataGetRequestSerializer(serializers.Serializer): child=serializers.IntegerField(), required=False, allow_null=True ) include_bots = serializers.BooleanField(required=False, allow_null=True) + only_bots = serializers.BooleanField(required=False, allow_null=True) anonymized = serializers.BooleanField(required=False) joined_before_date = serializers.DateTimeField(required=False) include_key_factors = serializers.BooleanField(required=False, default=False) @@ -139,6 +140,7 @@ def validate(self, attrs): "include_user_data", "user_ids", "include_bots", + "only_bots", "anonymized", "joined_before_date", "include_key_factors", @@ -152,13 +154,17 @@ def validate(self, attrs): aggregation_methods = attrs.get("aggregation_methods") user_ids = attrs.get("user_ids") include_bots = attrs.get("include_bots") + only_bots = attrs.get("only_bots") minimize = attrs.get("minimize", True) if not aggregation_methods and ( - (user_ids is not None) or (include_bots is not None) or not minimize + (user_ids is not None) + or (include_bots is not None) + or (only_bots is not None) + or not minimize ): raise serializers.ValidationError( - "If user_ids, include_bots, or minimize is set, " + "If user_ids, include_bots, only_bots, or minimize is set, " "aggregation_methods must also be set." ) diff --git a/utils/the_math/aggregations.py b/utils/the_math/aggregations.py index e481bebc3f..b2627bafc2 100644 --- a/utils/the_math/aggregations.py +++ b/utils/the_math/aggregations.py @@ -736,6 +736,7 @@ def get_aggregations_at_time( include_stats: bool = False, histogram: bool = False, include_bots: bool = False, + only_bots: bool = False, joined_before: datetime | None = None, ) -> dict[AggregationMethod, AggregateForecast]: """set include_stats to True if you want to include num_forecasters, q1s, medians, @@ -747,11 +748,13 @@ def get_aggregations_at_time( ) if only_include_user_ids: forecasts = forecasts.filter(author_id__in=only_include_user_ids) + elif only_bots: + forecasts = forecasts.filter(author__is_bot=True) else: # only include forecasts by non-primary bots if user ids explicitly specified forecasts = forecasts.exclude_non_primary_bots() - if not include_bots: - forecasts = forecasts.exclude(author__is_bot=True) + if not include_bots: + forecasts = forecasts.exclude(author__is_bot=True) if len(forecasts) == 0: return dict() forecast_set = ForecastSet( @@ -980,6 +983,7 @@ def get_aggregation_history( minimize: bool | int = True, include_stats: bool = True, include_bots: bool = False, + only_bots: bool = False, histogram: bool | None = None, include_future: bool = True, joined_before: datetime | None = None, @@ -999,11 +1003,13 @@ def get_aggregation_history( if only_include_user_ids: forecasts = forecasts.filter(author_id__in=only_include_user_ids) + elif only_bots: + forecasts = forecasts.filter(author__is_bot=True) else: # only include forecasts by non-primary bots if user ids explicitly specified forecasts = forecasts.exclude_non_primary_bots() - if not include_bots: - forecasts = forecasts.exclude(author__is_bot=True) + if not include_bots: + forecasts = forecasts.exclude(author__is_bot=True) if include_pre_predictions: earliest_time = None diff --git a/utils/views.py b/utils/views.py index bf426a1dde..13ebc17da5 100644 --- a/utils/views.py +++ b/utils/views.py @@ -74,6 +74,7 @@ def aggregation_explorer_api_view(request) -> Response: aggregation_methods = params.get("aggregation_methods", []) only_include_user_ids = params.get("user_ids") include_bots = params.get("include_bots") + only_bots = params.get("only_bots") or False minimize = params.get("minimize", True) joined_before = params.get("joined_before_date") @@ -88,6 +89,7 @@ def aggregation_explorer_api_view(request) -> Response: if include_bots is not None else question.include_bots_in_aggregates ), + only_bots=only_bots, histogram=True, include_future=False, joined_before=joined_before, @@ -107,6 +109,8 @@ def aggregation_explorer_api_view(request) -> Response: forecasters_qs = question.get_forecasters() if only_include_user_ids: forecasters_qs = forecasters_qs.filter(id__in=only_include_user_ids) + elif only_bots: + forecasters_qs = forecasters_qs.filter(is_bot=True) elif not include_bots: forecasters_qs = forecasters_qs.filter(is_bot=False) From 2a88af14704d9dc4770ea19173d05688f6fa2843 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:03:03 +0000 Subject: [PATCH 2/3] Resolve merge conflicts with main - aggregations.py: keep exclude_blacklisted_users() added in main alongside the new only_bots branch and updated comment. - en.json: pull in openOnMetaculus plus the laborHubJobs/midtermsHub keys that landed in main while keeping the new bot mode keys. Co-authored-by: Sylvain --- front_end/messages/en.json | 135 ++++++++++++++++++++++++++++++++- utils/the_math/aggregations.py | 8 +- 2 files changed, 140 insertions(+), 3 deletions(-) diff --git a/front_end/messages/en.json b/front_end/messages/en.json index ea6af01b84..d81996d3db 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -1980,6 +1980,7 @@ "copyQuestionLinkSuccess": "Question link successfully created! You can see them under the Question Links section for each question.", "viewQuestionLinks": "View Question Links", "moreActions": "More actions", + "openOnMetaculus": "Open on Metaculus", "copyQuestionLinkImpactPrefix": "has a", "copyQuestionLinkImpactSuffix": "causal impact on:", "copyQuestionLinkStrengthLabel": "The strength of this impact is:", @@ -2213,5 +2214,137 @@ "whyTrustMetaculusLessNoiseTitle": "Less noise, more truth", "whyTrustMetaculusLessNoise": "Metaculus's crowd-powered forecasts cut through the noise by grounding every prediction in transparent evidence, accountable scoring, and a decade of demonstrated accuracy. Metaculus equips policymakers, researchers, journalists, and corporate organizations with evidence-based forecasts that surface clear, quantifiable insight into the world's most critical uncertainties. Explore our suite of Business Solutions to learn more about how Metaculus can improve your organization's decision-making.", "publishTimeLockedDescription": "Publish time cannot be changed after creation.", - "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)" + "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", + "laborHubJobsBreadcrumb": "All Jobs", + "laborHubJobsVisitCta": "Visit Jobs", + "laborHubJobsPageTitle": "All Jobs · Labor Automation Forecasting Hub | Metaculus", + "laborHubJobsPageDescription": "Browse 15 US occupations forecasted by the Metaculus community through 2035. See how AI may reshape each one.", + "laborHubJobsHeroTitle": "Which jobs will survive AI?", + "laborHubJobsHeroLead": "Metaculus forecasters track 15 representative US occupations through 2035. Pick a job to see the forecast trajectory, the why behind the numbers, and what comes next.", + "laborHubJobDetailTitle": "Will AI replace {name}? · Metaculus Labor Hub forecast", + "laborHubJobDetailDescription": "Community forecasts for {name} employment, wages and AI exposure through 2035. Live from the Metaculus Labor Automation Forecasting Hub.", + "laborHubJobDetailSubtitle": "Community forecast of employment change versus 2025, across 2027, 2030, and 2035.", + "laborHubJobsClickTileSubtitle": "Click any tile for the full forecast & forecaster commentary", + "laborHubJobsYearToggleLabel": "Forecast year", + "laborHubJobsExploreCta": "Explore the full Labor Hub Dashboard", + "laborHubJobsExploreCtaSub": "15 occupations · 7 economy-wide forecasts · forecaster commentary", + "laborHubJobsByYear": "By {year}", + "laborHubJobsJumpToLabel": "Jump to:", + "laborHubJobsFeltenLabel": "Felten et al. exposure", + "laborHubJobsFeltenTooltip": "Academic measure of how much a job's required abilities overlap with current AI capabilities.", + "laborHubJobsMnaLabel": "M&A vulnerability", + "laborHubJobsMnaTooltip": "Academic measure combining a job's AI exposure with how well its workers can adapt.", + "laborHubJobsAoeLabel": "Anthropic Exposure", + "laborHubJobsAoeTooltip": "Grounded in real, anonymized Claude usage — the share of this job's tasks already being done with AI.", + "laborHubJobsRingHigh": "HIGH", + "laborHubJobsRingMed": "MED", + "laborHubJobsRingLow": "LOW", + "laborHubJobsCuratedInsightsTitle": "Curated insights", + "laborHubJobsCuratedInsightsEmpty": "No insights surfaced for {name} yet — check the full Labor Hub for active discussion.", + "laborHubJobsInsightSourceFromAnotherPost": "from a related question", + "laborHubJobsWagesTitle": "Median wage by 2035", + "laborHubJobsWagesSub": "Real change vs 2025", + "laborHubJobsWagesUnavailable": "Per-occupation wage forecast not yet tracked", + "laborHubJobsHoursTitle": "Hours / week · economy-wide", + "laborHubJobsHoursEconomyWideSub": "By 2035, vs {from} hrs/week in 2025", + "laborHubJobsShareTitle": "Share this forecast", + "laborHubJobsShareLead": "Download a share card and share this forecast with your network.", + "laborHubJobsShareYearLabel": "Headline year", + "laborHubJobsShareYearOption": "By {year}", + "laborHubJobsShareSave": "Save Image", + "laborHubJobsShareTweet": "Share on X", + "laborHubJobsShareLinkedin": "Share on LinkedIn", + "laborHubJobsMetricSeeAll": "Click to compare this to other jobs →", + "laborHubJobsMetricPaperLink": "Read the paper →", + "laborHubJobsProForecasterTitle": "Pro Forecaster", + "laborHubJobsMetricChartHead": "How compares", + "laborHubJobsMetricLower": "lower", + "laborHubJobsMetricHigher": "higher", + "laborHubJobsFeltenSource": "Felten, Raj & Seamans — Language modeling AIOE", + "laborHubJobsFeltenAxisLabel": "Felten exposure score (standardized)", + "laborHubJobsFeltenNature1": "The AI Occupational Exposure (AIOE) measures the overlap between AI capabilities and the abilities an occupation requires, aggregated from O*NET ability data.", + "laborHubJobsFeltenNature3": "Higher exposure does not by itself mean a job will be automated — it can equally signal that AI augments the work.", + "laborHubJobsFeltenBounds": "Scale: standardized z-score · the average occupation sits near 0; positive values are more exposed than average, negative values less.", + "laborHubJobsMnaSource": "Manning & Aguirre — Vulnerability score", + "laborHubJobsMnaAxisLabel": "Manning & Aguirre vulnerability (0–1)", + "laborHubJobsMnaNature1": "Academic measure combining exposure to AI with workers' adaptive capacity.", + "laborHubJobsMnaNature3": "Vulnerability scores are related both to the likelihood of displacement (through AI exposure) and its burden (through adaptive capacity).", + "laborHubJobsMnaBounds": "Scale: 0 to 1 · highest in occupations that are both heavily exposed and their workers are poorly cushioned against displacement.", + "laborHubJobsAoeSource": "Massenkoff & McCrory — Observed Exposure (AOE)", + "laborHubJobsAoeAxisLabel": "Observed AI exposure (0–100%)", + "laborHubJobsAoeNature1": "Unlike purely theoretical measures, this is also grounded in real, anonymized Claude usage: what people are actually doing with AI right now, mapped to O*NET occupation tasks.", + "laborHubJobsAoeNature3": "AOE captures observed adoption, not potential — a low value may mean the work simply isn't being brought to Claude yet, not that AI couldn't do it.", + "laborHubJobsAoeBounds": "Scale: 0% to 100% · highest in occupations whose tasks AI can theoretically do and is already being used for.", + "laborHubJobsChartA11y": "Employment forecast by year", + "laborHubJobsChartBaseline": "Baseline", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congressional Control", + "midtermsHubChamberCurrent": "Current:", + "midtermsHubChamberForecast": "Forecast:", + "midtermsHubChamberTooltipBody": "Forecasters give {party} a {pct}% chance of holding the most {chamber} seats.", + "midtermsHubChamberTooltipDisclaimer": "Click to view forecasting question", + "midtermsHubPartyDemocrats": "Democrats", + "midtermsHubPartyRepublicans": "Republicans", + "midtermsHubOutcomeRepRep": "Rep House / Rep Senate", + "midtermsHubOutcomeRepDem": "Dem House / Rep Senate", + "midtermsHubOutcomeDemRep": "Rep House / Dem Senate", + "midtermsHubOutcomeDemDem": "Dem House / Dem Senate", + "midtermsHubCongressSummary": "Forecasters' current views on Congress control are reflected in the probabilities above.", + "midtermsHubThingsToWatch": "Key Drivers", + "midtermsHubAbortionAmendment": "Missouri abortion-rights repeal", + "midtermsHubAbortionAmendmentContext": "Missouri voters decide whether to repeal the abortion-rights protections they enshrined in 2024.", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceHeaderRepTitle": "Republican Congress", + "midtermsHubConsequenceHeaderRepSubtitle": "If GOP controls", + "midtermsHubConsequenceHeaderSplitTitle": "Split Congress", + "midtermsHubConsequenceHeaderSplitSubtitle": "If control is split", + "midtermsHubConsequenceIfSplit": "if Split Congress", + "midtermsHubMailInBallots": "Mail-in ballot court challenge", + "midtermsHubMailInBallotsContext": "New USPS restrictions could reshape mail-in voting access, but pending lawsuits may block them before November.", + "midtermsHubElectionEmergency": "Election-integrity national emergency", + "midtermsHubElectionEmergencyContext": "A national emergency declaration could expand federal authority over the election.", + "midtermsHubConsequenceHeaderDemTitle": "Democratic Congress", + "midtermsHubConsequenceHeaderDemSubtitle": "If Dems control", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubFooterDisclaimer": "Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Forecasts from the Metaculus community on contested races and the policies they will shape.", + "midtermsHubHeroSubtitleMobile": "Real-time forecasts from the Metaculus community.", + "midtermsHubConsequencesSubtitle": "How forecasted outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated in real time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view question", + "midtermsHubSeatDistributionsTitle": "Seat Distributions", + "midtermsHubSeatDistributionsSubtitle": "The community's forecasts for the final seat advantage in each chamber.", + "midtermsHubSenateSeats": "Senate", + "midtermsHubHouseSeats": "House", + "midtermsHubDemSeatAdvantage": "Democratic Seat Advantage", + "midtermsHubRepSeatAdvantage": "Republican Seat Advantage", + "midtermsHubEven": "EVEN", + "midtermsHubForecastUnavailable": "Forecast unavailable", + "midtermsHubSeatAdvantageTooltip": "{count} seat advantage", + "midtermsHubProbabilityTooltip": "{value}% probability", + "midtermsHubEvenTooltip": "Even" } diff --git a/utils/the_math/aggregations.py b/utils/the_math/aggregations.py index b2627bafc2..a2177b6c24 100644 --- a/utils/the_math/aggregations.py +++ b/utils/the_math/aggregations.py @@ -751,8 +751,10 @@ def get_aggregations_at_time( elif only_bots: forecasts = forecasts.filter(author__is_bot=True) else: - # only include forecasts by non-primary bots if user ids explicitly specified + # only include forecasts by non-primary bots or blacklisted users + # if user ids explicitly specified forecasts = forecasts.exclude_non_primary_bots() + forecasts = forecasts.exclude_blacklisted_users() if not include_bots: forecasts = forecasts.exclude(author__is_bot=True) if len(forecasts) == 0: @@ -1006,8 +1008,10 @@ def get_aggregation_history( elif only_bots: forecasts = forecasts.filter(author__is_bot=True) else: - # only include forecasts by non-primary bots if user ids explicitly specified + # only include forecasts by non-primary bots or blacklisted users + # if user ids explicitly specified forecasts = forecasts.exclude_non_primary_bots() + forecasts = forecasts.exclude_blacklisted_users() if not include_bots: forecasts = forecasts.exclude(author__is_bot=True) From e52ac5030e3a9b9da899abc10be60ea9dcb07da0 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:43:19 +0000 Subject: [PATCH 3/3] Trim en.json to only the 3 bot-mode keys added by this PR Address review feedback: the previous merge conflict resolution pulled in ~130 unrelated translation keys (labor hub, midterms hub, openOnMetaculus) and left several keys stale relative to main. Rebase en.json onto origin/main and re-add only excludeBots, onlyBots, and bots next to includeBots. Co-authored-by: Sylvain <74110469+SylvainChevalier@users.noreply.github.com> --- front_end/messages/en.json | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/front_end/messages/en.json b/front_end/messages/en.json index d81996d3db..b6c650a5c1 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -236,6 +236,7 @@ "metaculus_prediction": "Metaculus Prediction", "metaculus": "Metaculus", "community": "community", + "enterProbability": "Enter probability", "communityPrediction": "Community Prediction", "unweightedAggregate": "Unweighted Aggregate", "firstQuartile": "lower 25%", @@ -273,6 +274,8 @@ "commentsReadGuidelines": "Read our guidelines", "commentsLearnMarkdown": "Learn about our Markdown", "commentDeleted": "Comment deleted", + "deleteCommentConfirmTitle": "Delete comment?", + "deleteCommentConfirmDescription": "This comment will be removed. This can't be undone.", "commentsReportCommentHeading": "Report this comment", "commentsReportCommentDescription": "Is there something wrong with this comment? Please help us keep our community great by reporting it.", "commentsReportSpam": "Report as spam", @@ -354,6 +357,7 @@ "searchOptionActivePrediction": "Active Prediction", "searchOptionWithdrawnPrediction": "Withdrawn", "searchOptionNotPredicted": "Not Predicted", + "searchOptionCommented": "Commented", "searchOptionAuthored": "Authored", "searchOptionUpvoted": "Upvoted", "searchOptionPrivate": "Private", @@ -388,6 +392,7 @@ "unreadWithTotalCount": "({unread_count_formatted} unread) {total_count_formatted} total", "unreadWithTotalCountXs": "({unread_count_formatted}) {total_count_formatted}", "forecastersWithCount": "{count_formatted} {count, plural, =1 {forecaster} other {forecasters}}", + "histogramBinShare": "{share} weight of forecasts", "or": "or", "signupInviteUsers": "Invite Users", "signupInviteUsersDescription": "Enter the email addresses of users you would like to invite, separated by newlines. They will receive an invitation to sign up by email.", @@ -493,6 +498,7 @@ "reminderErrorPercentLifetimeRequired": "Please set a percent of lifetime", "reminderErrorPercentLifetimeCannotHappen": "This would result in no notification, please select a lower value", "reminderErrorPercentLifetimeWouldNotRepeat": "This would result in no repetition. Consider switching to \"Doesn't Repeat\"", + "reminderErrorDuplicateDate": "This date is already selected. Please choose a different date.", "reminderNotePlaceholder": "Write yourself a note...", "reminderCPChangesDescription1": "Notify me when the forecast changes by more than 10%", "reminderCPChangesDescription2": "For continuous questions, this is measured by a Kolmogorov–Smirnov test.", @@ -854,6 +860,7 @@ "userProfile": "{username}'s profile", "unknownUserProfile": "User's profile", "userMedals": "{username}'s medals", + "userContributions": "{username}'s contributions", "unknownUserMedals": "User's medals", "noMedals": "No medals yet", "leaderboard": "Leaderboard", @@ -908,6 +915,12 @@ "parentBackgroundInfo": "Parent Question Background Info", "childBackgroundInfo": "Child Question Background Info", "histogram": "Histogram", + "distributions": "Distributions", + "quartileLabelP25": "25th %", + "quartileLabelMedian": "Median", + "quartileLabelP75": "75th %", + "pdfCdfExplanation": "PDF (Probability Density Function) shows how likely different outcomes are around specific values, while CDF (Cumulative Distribution Function) shows the cumulative probability of outcomes up to a certain value.", + "pmfCdfExplanation": "PMF (Probability Mass Function) shows how likely different specific outcomes are, while CDF (Cumulative Distribution Function) shows the cumulative probability of outcomes up to a certain value.", "frequency": "Frequency", "brierScoreForPlayer": "Brier scores for player predictions", "scoreHistogram": "Score Histogram", @@ -919,6 +932,7 @@ "parentResolutionCriteria": "Parent Resolution Criteria", "childResolutionCriteria": "Child Resolution Criteria", "loadMoreComments": "Load more comments", + "linkedComment": "Linked comment", "followers": "Followers", "followed": "Followed", "followButton": "Follow", @@ -1003,7 +1017,7 @@ "settingsNewEmail": "New Email", "settingsEmailChangeDescription": "Enter a new email address and a password, and we'll send you a link to change your email.", "settingsChangeEmailAddress": "Change email address", - "settingsChangeEmailAddressSuccess": "A confirmation email has been sent to your new address. Please follow the link inside to activate it.", + "settingsChangeEmailAddressSuccess": "A confirmation email has been sent to your old address. Please follow the link inside to activate it.", "emailChangeErrorMessage": "Failed to change email", "emailChangeSuccessMessage": "Your email address has been changed successfully", "socialAccountNoPasswordBanner": "Your account doesn't have a password yet. Set one below to change your email or log in with email and password.", @@ -1337,6 +1351,8 @@ "keyFactor": "Key Factor", "scrollToKeyFactor": "Scroll to key factor", "keyFactors": "Key Factors", + "keyFactorsFor": "Key factors for", + "goToKeyFactor": "Go to key factor {number}", "topKeyFactors": "Top Key Factors", "notAvailable": "Not available", "indexesTitle": "Project", @@ -1397,6 +1413,7 @@ "selectChildQuestion": "Select Child Question", "pickQuestion": "Pick Question", "viewQuestions": "View Questions({count})", + "includingSubquestions": "Including Subquestions: {count}", "predictionFlow": "Prediction Flow", "started": "Started", "starts": "Starts", @@ -2043,6 +2060,7 @@ "tournamentTimelineStarts": "Starts {when}", "tournamentTimelineEnds": "Ends {when}", "tournamentTimelineClosed": "Pending resolutions", + "tournamentTimelinePendingWinners": "Pending winners announcement", "tournamentTimelineAllResolved": "All questions resolved", "tournamentRelativeSoon": "soon", "tournamentRelativeUnderMinute": "in under a minute", @@ -2227,7 +2245,7 @@ "laborHubJobsClickTileSubtitle": "Click any tile for the full forecast & forecaster commentary", "laborHubJobsYearToggleLabel": "Forecast year", "laborHubJobsExploreCta": "Explore the full Labor Hub Dashboard", - "laborHubJobsExploreCtaSub": "15 occupations · 7 economy-wide forecasts · forecaster commentary", + "laborHubJobsExploreCtaSub": "Occupations and wages · economy-wide forecasts · forecaster commentary", "laborHubJobsByYear": "By {year}", "laborHubJobsJumpToLabel": "Jump to:", "laborHubJobsFeltenLabel": "Felten et al. exposure", @@ -2345,6 +2363,7 @@ "midtermsHubEven": "EVEN", "midtermsHubForecastUnavailable": "Forecast unavailable", "midtermsHubSeatAdvantageTooltip": "{count} seat advantage", + "midtermsHubSeatAdvantageOverTooltip": ">{count} seat advantage", "midtermsHubProbabilityTooltip": "{value}% probability", "midtermsHubEvenTooltip": "Even" }