ETL Pipeline for Multi-Source Bibliographic Data#26
Open
anibihakeem wants to merge 13 commits into
Open
Conversation
…t co-author semicolons)
… feeding the shared pipeline
…r for correct list splitting
…rking with live OpenAlex data
…, DP->PY and DOI cleanup
… now supported on both file and API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ETL Pipeline for Multi-Source Bibliographic Data
Sources: Scopus · Dimensions · PubMed · OpenAlex
1. The Problem
Every analytical function in
functions/andservices/assumes its input is already in the Web of Science (WoS) internal format: WoS column tags (AU, PY, SO...), WoS types, WoS conventions for multi-value fields. Scopus, Dimensions, PubMed, and OpenAlex each use different column names, file formats, delimiters, and representations of authors, affiliations, and references. Feed any of them to the dashboard unchanged and it either crashes or quietly returns wrong results.This PR adds the translation layer: a source-agnostic ETL pipeline that converts any supported source into the exact schema the analytical functions expect. It's the Python counterpart to
convert2df()in the R bibliometrix package.2. What This PR Delivers
Four sources, five ways in, one pipeline:
All five paths run through the same transformation code. Adding another source later means writing one mapping dictionary, and nothing else.
New modules:
www/services/standardizer.py: the transform layer (dispatch, mappings, type contracts, validation, entry point, export)www/services/api_retriever.py: live retrieval for OpenAlex and PubMedtests/test_pipeline.py: reproducible smoke testssources/samples/: raw and standardized sample datasetsPlus fixes to seven existing functions that broke on standardized non-WoS data (§5).
3. How the Pipeline Works
A single call runs the whole thing:
Extract. Each file type gets the right loader:
pd.read_csv, a Dimensions-awareload_dimensions_xlsxthat skips the vendor disclaimer row, or the repo's existingparse_pubmed_dataMEDLINE parser. API sources are fetched live (§4).Transform.
standardize(raw_df, source)runs four steps:SOURCE_REGISTRYmaps each source to its(mapping_dict, DB_label, delimiter). The registry is the only place routing happens, so there's noif source == …scattered through the transform logic."Cited by" → TC, Dimensions"PubYear" → PY, PubMed"TA" → JI). I built these by reading real exports, not the docs.Paginationfield ("429-468"), which splits intoBP/EP; PubMed'sDP("2026 Jul 3") becomes a 4-digitPY; PubMed'sLID/AIDclean down to theDIfield.AU, AF, C1, CR, DE, ID) becomelist[str];PYandTCbecomeint(nulls → 0); everything else becomesstr(nulls →""). Nothing survives as NaN or None, and that one guarantee heads off a whole category of downstream crashes.SR derivation. The Short Reference comes from calling the repo's existing
metaTagExtraction(df, "SR"). Reused, not rewritten.Validate.
validate()checks the output directly: complete schema, no nulls, list-columns that actually hold lists, int-columns that are actually integers. If something's off, it stops there instead of passing bad data downstream.Load.
export_standardized()writes dashboard-compatible XLSX/CSV. It joins list-columns with the codebase's own;delimiter and writes empty cells as"", never NaN, because Excel reads NaN back as a float and that float crashes anything that iterates the cell.4. Live API Retrieval
Beyond file imports,
api_retriever.pyadds live fetching from two separate APIs. The user gives a search string and picks a platform, and gets back records ready for the pipeline.OpenAlex (
fetch_openalex(query, max_records)):OPENALEX_API_KEY, never hardcoded or committed (OpenAlex made keys mandatory in Feb 2026)bibliointo volume/issue/pages, and the inverted-index abstract back into plain textPubMed (
fetch_pubmed(query, max_records)):esearchfor PMIDs,efetchfor the MEDLINE recordsparse_pubmed_data(), so the file path and the API path share one parser and PubMed parsing lives in exactly one placeBoth feed the same
run_pipeline()as everything else, so no transformation logic is duplicated across the five paths.5. Fixes to Existing Functions
Standardized data surfaced real bugs in the analytical layer. I fixed each one where it belonged: centrally in the pipeline when the fix helps every function, or in the specific function when the bug lived there.
PY.max() - PY.min()on string years raisedTypeError. AddingPYto the int contract fixed it once, centrally, instead of per function.Affiliationsfield breaks the link between an author and their affiliation, so I remappedC1toAuthors with affiliations. (b) Scopus writes "Viet Nam" whilecountries.txthas "VIETNAM", so aCOUNTRY_NORMALIZATIONmap canonicalizes affiliation strings before extraction.;both inside a reference and between references, so I split on(?<=\));\s+(a year boundary) instead of every semicolon.invalid syntax. Both raneval()on cell contents, which is unsafe and blows up on a plain;-string. Replaced with tolerant deserialization: take a real list as-is, parse a list-repr withast.literal_eval, otherwise split on;.AUwas quietly swapped for[], thenint(NaN)in the axis math crashed. Same tolerant deserialization, plus a NaN guard.AU_CO/AU1_CO). The code walksC1expecting a list, but a file-loaded string iterates character by character. Now it handles both.for x in cellraises'float' object is not iterable. The export layer now writes"", which removes the float path.One thing ran through all of these: the codebase deserializes the same multi-value fields three different ways, split-on-
;,eval(), and silent discard. These patches bring them together: lists in memory,;-strings in files, both accepted everywhere. The remainingeval()calls should move toast.literal_eval, since running file contents as code is a security hole.6. Verification in the Actual Shiny Dashboard
I loaded standardized datasets from all four sources, Scopus (CSV), Dimensions (500 records), PubMed (MEDLINE), and OpenAlex (100, live), into the running dashboard and went through it panel by panel.
Works across all four sources (16 panels)
Main Information · Average Citations per Year · Three-Field Plot · Annual Scientific Production · Most Relevant Sources · Bradford's Law · Sources' Production over Time · Sources' Local Impact · Most Relevant Authors · Authors' Production over Time · Authors' Local Impact · Countries' Scientific Production · Countries' Production over Time · Most Frequent Words · Word Cloud · Collaboration Network.
That covers production, sources, authors, countries, words, and networks, so the schema holds up the same way across CSV, XLSX, MEDLINE, and JSON.
The image above shows "Countries Collaboration Network", generated from OpenAlex data (100 records, fetched live via the API). Collaboration arcs connect countries extracted from the standardized C1 field
The image above shows "Countries' Production over Time" generated from OpenAlex data. Cumulative publication counts by country (USA, France, Germany, Netherlands, China) plotted correctly across a 1988–2021 range.
Works where the source provides the data
RPfieldNot verified
'float' object is not iterable, the same empty-cell issue as §5.7. Follow-up patch candidate.python -m tests.test_pipelinereproduces the standardized output behind these panels.7. Known Limitations
The direct-citation family doesn't run for non-WoS sources, for two separate reasons.
First, the port gates it on purpose:
histNetworkchecks the database and returnsNonewith "Database not compatible with direct citation analysis" for non-WoS data, and the callers then crash on that unguardedNone. Second, and more to the point, the data isn't there. These analyses need references that can be matched back to the collection's own SR keys, and no source provides that usably: Dimensions exports no references, OpenAlex gives opaque work-IDs instead of citation strings, and Scopus reference strings can't be decomposed reliably.So this is a limit of the source data rather than the schema, and it's out of scope here. The natural next step is adding None-guards so these panels show a clear message instead of erroring out.
Minor: per-reference source extraction (
CR_SO) on Scopus is unreliable because of the same comma-ambiguity, and the author-collaboration plot errors on degenerate graphs when a min-edge threshold empties a small sample, though the underlying matrix computes fine.8. How to Run