EvalReader
def EvalReader(
cfg:dict, # Configuration dict with field mappings and processing rules
):Interface for a reader that turns a repository export into Evaluations
Evaluation records, save them as JSON, and find an evaluation and its report URL
The IOM evaluation repository exports its search results as a CSV file, from evaluation.iom.int/evaluation-search-pdf. The CSV has one row per evaluation. IOMRepoReader turns each row into an Evaluation, with an id, a list of documents and the other columns as metadata. IOMRepoReader.to_json saves the evaluations as JSON, and load_evals reads them back. find_eval looks an evaluation up by title, URL or ID, and eval_url gives the URL of its report PDF.
Interface for a reader that turns a repository export into Evaluations
class EvalReader:
"Interface for a reader that turns a repository export into `Evaluation`s"
def __init__(self,
cfg:dict # Configuration dict with field mappings and processing rules
): store_attr()
def read(self): raise NotImplementedError
def tfm(self, df): raise NotImplementedError
def to_json(self, output_path): raise NotImplementedError
def __call__(self):
df = self.read()
return self.tfm(df)EvalReader is the interface for repository readers. A reader takes a config dict. Calling it runs read, then tfm. IOMRepoReader is the only implementation.
def iom_input_cfg():
"Config for reading IOM CSV exports"
return {
'date_cols': ['Date of Publication', 'Evaluation Period From Date', 'Evaluation Period To Date'],
'string_cols': ['Year'],
'list_fields': {
'Countries Covered': {'separator': ',', 'clean': True}
},
'document_fields': ['Document Subtype', 'File URL', 'File description'],
'id_gen': {
'method': 'md5',
'fields': ['Title', 'Year', 'Project Code'] # fields to hash
},
'field_mappings': {
'Title': 'title',
'Year': 'year',
# other mappings
}
}An evaluation and its documents, displayed as a summary card in notebooks
@dataclass
class Evaluation:
"An evaluation and its documents, displayed as a summary card in notebooks"
id:str # MD5 hash of the evaluation's `Title`, `Year` and `Project Code`
docs:list # Documents, each a dict with `subtype`, `url` and `desc`
meta:dict # The other CSV columns, such as `Title`, `Year` and `Countries Covered`
def _repr_markdown_(self):
title = self.meta.get('Title', 'Untitled')
year = self.meta.get('Year', 'n/a')
org = self.meta.get('Evaluation Commissioner', 'Unknown')
countries = self.meta.get('Countries Covered', [])
country_str = ', '.join(countries[:3]) if countries else 'Not specified'
if len(countries) > 3: country_str += f' (+{len(countries)-3} more)'
return f"""
### {title}
**Year:** {year} | **Organization:** {org} | **Countries:** {country_str}
**Documents:** {len(self.docs)} available
**ID:** `{self.id}`
"""Read an IOM evaluation CSV export into Evaluations
The test export in files/test is an IOM CSV export. read loads it as it is, one row per evaluation:
Title EVALUATION OF IOM’S MIGRATION DATA STRATEGY
Year 2025
Author IOM CENTRAL EVALUATION
Best Practicesor Lessons Learnt Yes
Date of Publication 2025-08-11
Donor IOM
Evaluation Brief Yes
Evaluation Commissioner IOM
Evaluation Coverage Global
Evaluation Period From Date 2025-08-20
Evaluation Period To Date 2025-05-31
Executive Summary Yes
External Version of the Report No
Languages English
Migration Thematic Areas Organisational policy/strategy
Name of Project(s) Being Evaluated NaN
Number of Pages Excluding annexes 44.0
Other Documents Included NaN
Project Code NaN
Countries Covered Worldwide
Regions Covered HQ Geneva
Relevant Crosscutting Themes NaN
Report Published Yes
Terms of Reference Yes
Type of Evaluation Scope Strategy
Type of Evaluation Timing Not applicable
Type of Evaluator External
Level of Evaluation Centralized
Document Subtype Evaluation report, Evaluation brief, Annexes, ...
File URL https://evaluation.iom.int/sites/g/files/tmzbd...
File description Evaluation Report, Evaluation Brief, Annex VI ...
Management response No
Date added Thu, 08/07/2025 - 23:52
Name: 0, dtype: object
Title EVALUATION OF IOM’S MIGRATION DATA STRATEGY
Year 2025
Author IOM CENTRAL EVALUATION
Best Practicesor Lessons Learnt Yes
Date of Publication 2025-08-11
Donor IOM
Evaluation Brief Yes
Evaluation Commissioner IOM
Evaluation Coverage Global
Evaluation Period From Date 2025-08-20
Evaluation Period To Date 2025-05-31
Executive Summary Yes
External Version of the Report No
Languages English
Migration Thematic Areas Organisational policy/strategy
Name of Project(s) Being Evaluated NaN
Number of Pages Excluding annexes 44.0
Other Documents Included NaN
Project Code NaN
Countries Covered Worldwide
Regions Covered HQ Geneva
Relevant Crosscutting Themes NaN
Report Published Yes
Terms of Reference Yes
Type of Evaluation Scope Strategy
Type of Evaluation Timing Not applicable
Type of Evaluator External
Level of Evaluation Centralized
Document Subtype Evaluation report, Evaluation brief, Annexes, ...
File URL https://evaluation.iom.int/sites/g/files/tmzbd...
File description Evaluation Report, Evaluation Brief, Annex VI ...
Management response No
Date added Thu, 08/07/2025 - 23:52
Name: 0, dtype: object
Three columns describe an evaluation’s documents. Each holds a comma-separated list, with one entry per document:
'Evaluation report, Evaluation brief, Annexes, Annexes, Annexes, Special related reports/documents'
'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Annex%20VI%20Case%20Study%20-%20RDH%20East%2C%20Horn%20and%20Southern%20Africa.pdf, https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Annex%20VII%20Case%20Study%20-%20RDH%20Asia-Pacific.pdf, https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Annex%20VIII%20-%20Inception%20Report.pdf, https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Evaluation%20Brief.pdf, https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/IOM%20MDS%20Evaluation%20Report%20-%20clean_0.pdf, https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Migration%20Data%20Evaluation%20infographics.pdf'
'Evaluation Report, Evaluation Brief, Annex VI Case Study - RDH East, Horn and Southern Africa, Annex VII Case Study - RDH Asia-Pacific, Annex VIII - Inception Report, Infographics'
The CSV has no ID column. _mk_id hashes the Title, Year and Project Code of a row with MD5. An evaluation gets the same ID in every export, as long as those three fields stay the same:
@patch
def _mk_docs(self:IOMRepoReader,
row # DataFrame row with document fields
):
"Pair the document columns of `row` into records"
stypes = [s.strip() for s in str(row['Document Subtype']).split(', ')]
urls = [u.strip() for u in str(row['File URL']).split(', ')]
descs = [d.strip() for d in str(row['File description']).split(', ')]
return [dict(subtype=st, url=u, desc=d) for st,u,d in zip(stypes,urls,descs) if u.strip()]An evaluation can have more than one document, such as a report, a brief and annexes. The three document columns list them in the same order. _mk_docs pairs the entries into records with subtype, url and desc keys, and drops entries without a URL. It splits the columns on ,. A subtype, URL or description that contains , shifts the pairing.
[{'subtype': 'Evaluation report',
'url': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Annex%20VI%20Case%20Study%20-%20RDH%20East%2C%20Horn%20and%20Southern%20Africa.pdf',
'desc': 'Evaluation Report'},
{'subtype': 'Evaluation brief',
'url': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Annex%20VII%20Case%20Study%20-%20RDH%20Asia-Pacific.pdf',
'desc': 'Evaluation Brief'},
{'subtype': 'Annexes',
'url': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Annex%20VIII%20-%20Inception%20Report.pdf',
'desc': 'Annex VI Case Study - RDH East'},
{'subtype': 'Annexes',
'url': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Evaluation%20Brief.pdf',
'desc': 'Horn and Southern Africa'},
{'subtype': 'Annexes',
'url': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/IOM%20MDS%20Evaluation%20Report%20-%20clean_0.pdf',
'desc': 'Annex VII Case Study - RDH Asia-Pacific'},
{'subtype': 'Special related reports/documents',
'url': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/Migration%20Data%20Evaluation%20infographics.pdf',
'desc': 'Annex VIII - Inception Report'}]
tfm cleans the columns before it builds the Evaluations. _proc_dates stores the date columns as strings, and missing dates stay missing. _proc_lists splits Countries Covered into a list of country names.
'2025-08-11'
@patch
def _proc_lists(self:IOMRepoReader, df):
"Split each of the `list_fields` of `df` into a list of trimmed, non-empty values"
for fname,fcfg in self.cfg['list_fields'].items():
vals = df[fname].fillna('').astype(str).str.split(fcfg['separator'])
df[fname] = vals.apply(lambda x: [item.strip() for item in x if item.strip()])
return df['Austria', 'Greece', 'Italy', 'Malta', 'Poland', 'Romania', 'Spain']
Turn the raw DataFrame into a list of Evaluations
@patch
def tfm(self:IOMRepoReader, df:pd.DataFrame):
"Turn the raw DataFrame into a list of `Evaluation`s"
df_proc = self._proc_lists(self._proc_dates(df.copy()))
df_proc['id'] = df_proc.apply(self._mk_id, axis=1)
df_proc['docs'] = df_proc.apply(self._mk_docs, axis=1)
return [self._to_eval(row) for _,row in df_proc.iterrows()]Calling the reader runs read, then tfm. Each Evaluation displays as a summary card:
IOM’s UNEG evaluation API also lists each evaluation’s report. get_report_urls maps each evaluation title to the URL of its first document whose name starts with Evaluation report. get_uneg_url looks up an Evaluation in that map by its Title.
Map each evaluation title to its report URL, from IOM’s UNEG evaluation API
{'IAHE Synthesis Report: A Synthesis and Meta-Analysis of Inter-Agency Humanitarian Evaluations (2015–2025)': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/iahe-synthesis-report-1.pdf',
'WESTERN BALKANS ASSISTED VOLUNTARY RETURN AND REINTEGRATION PROGRAMME PHASE II': 'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/WBRR%20Phase%20II%20Final%20Evaluation%20Report%20%2827%20June%202025%29%20%281%29.pdf'}
For instance:
'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/IOM%20MDS%20Evaluation%20Report%20-%20clean_0_1.pdf'
Save the evaluations as JSON at out_path, with their UNEG report URLs
to_json saves the evaluations as JSON at out_path. When the UNEG API has a report for an evaluation, to_json adds it as the first document, with subtype 'Evaluation report (UNEG)'. eval_url prefers that document.
To use the reader:
Then save them with to_json:
default_config names the fields that the downloaders read from an Evaluation: id, docs, and each document’s url.
Load the evaluations saved in json_file
Check whether url is one of the documents of ev
url = "https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/AAP%20Evaluation%20Report_final_.pdf"
fname = 'files/test/evaluations.json'
evals = load_evals(fname)
ev = first(evals.filter(lambda x: x.id == '6c3c2cf3fa479112967612b0baddab72'))
test_eq(in_docs(ev, url), True)
test_eq(in_docs(ev, "https://fake.url/nothere.pdf"), False)Find an evaluation by title, document URL or ID
def find_eval(
evals:list, # Evaluations to search
query:str, # Title, document URL or ID to look for
by:str='title' # What `query` is: `'title'`, `'url'` or `'id'`
) -> Evaluation: # The first matching evaluation, or `None`
"Find an evaluation by title, document URL or ID"
if by == 'title': return first([o for o in evals if o.meta['Title'] == query])
if by == 'url': return first([o for o in evals if in_docs(o, query)])
if by == 'id': return first([o for o in evals if o.id == query])title = 'Evaluation of IOM Accountability to Affected Populations'
url = "https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/AAP%20Evaluation%20Report_final_.pdf"
test_eq(find_eval(evals, title, by='title').id, '6c3c2cf3fa479112967612b0baddab72')
test_eq(find_eval(evals, url, by='url').id, '6c3c2cf3fa479112967612b0baddab72')
test_eq(find_eval(evals, 'Nonexistent Title', by='title'), None)
test_eq(find_eval(evals, 'https://fake.url/nowhere.pdf', by='url'), None)
test_eq(find_eval(evals, '6c3c2cf3fa479112967612b0baddab72', by='id').meta['Title'], 'Evaluation of IOM Accountability to Affected Populations')Year: 2025 | Organization: IOM | Countries: Worldwide
Documents: 5 available
ID: 6c3c2cf3fa479112967612b0baddab72
URL of the evaluation report of ev, preferring the UNEG report
eval_url returns the UNEG report that to_json added, when there is one. Otherwise it returns the first document whose subtype contains evaluation report. It logs a warning when it finds no report, and when the report URL does not end in .pdf.
'https://evaluation.iom.int/sites/g/files/tmzbdl151/files/docs/resources/AAP%20Evaluation%20Report_final_.pdf'
Year: 2025 | Organization: IOM | Countries: Worldwide
Documents: 3 available
ID: 1700c8dbadc3d87d6911a8ccc6d18b63