Skip to content

Instantly share code, notes, and snippets.

@psychemedia
Last active December 16, 2019 12:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save psychemedia/14272d065a5e5246acd648868e5599f5 to your computer and use it in GitHub Desktop.
Save psychemedia/14272d065a5e5246acd648868e5599f5 to your computer and use it in GitHub Desktop.
First fumblings of a sketch around profiling a Jupyter notebook as a text, looking at text readability metrics, code complexity metrics, etc
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Notebook Profiles\n",
"\n",
"This exploratory coding notebook explores several techniques to support the static profiling of Jupyter notebooks as texts, reporting on various metrics, including:\n",
"\n",
"- notebook size (markdown and code line counts);\n",
"- readability scores;\n",
"- reading time estimates;\n",
"- code complexity and maintability.\n",
"\n",
"The motivating context was a tool for generating summary reports on the estimated workload associated with 100 or so notebooks over 25 or so directories (1 directory / 4 notebooks per week) for a third year undergraduate equivalent Open University course on data management and analysis.\n",
"\n",
"Previous notebook recipes include generating simple reports that pull out headings from notebooks to act as notebook summaries (eg [`Get Contents`](https://github.com/innovationOUtside/TM351_forum_examples/blob/master/Get%20Contents.ipynb)). Such recipes may provide a useful component in a notebook quality report if the report is also intended to provide a summary / overview of notebooks. (It might be most useful to offer heading summaries as an option in a notebook profiling report?)\n",
"\n",
"Tools supporting the profiling of one or more notebooks across one or more directories and the generation of simple statistics over them are also provided.\n",
"\n",
"The profiler is also capabale of running simple health checks over a notebook, for example reporting on:\n",
"\n",
"- whether code cells have been executed, and if so, whether code cell execution in complete and in linear order;\n",
"- packages / modules loaded in to the notebook;\n",
"- unused code items in a notebook (for example, modules loaded but not used).\n",
"\n",
"Currently, code profiling is only applied to code that appears in code cells, not code that is quoted or described in markdown cells. \n",
"\n",
"There is a potential for making IPython magics for some of the reporting functions (for example, `radon` or `wily` reports) to provide live feedback / reporting during the creation of content in a notebook.\n",
"\n",
"### Notebooks\n",
"\n",
"In the first instance, reports are generated for code cell inputs and markdown cells; code outputs and raw cells are not considered. Code appearing in markdown cells is identified as code-like but not analysed in terms of code complexity etc.\n",
"\n",
"For each markdown cell, we can generate a wide range of simple text document statistics. Several packages exist to support such analyses (for example, [`textstat`](https://github.com/shivam5992/textstat), [`readability`](https://github.com/andreasvc/readability/)) but the focus in this notebook will be on metrics derived using the [`spacy`](https://spacy.io/) underpinned [`textacy`](https://github.com/chartbeat-labs/textacy) package for things like [readability](https://chartbeat-labs.github.io/textacy/api_reference/misc.html?highlight=readability#text-statistics) metrics. Several simple custom metrics are also suggested.\n",
"\n",
"For code in code cells, the [`radon`](https://radon.readthedocs.io) package is used to generate code metrics, with additional packages providing further simple metrics.\n",
"\n",
"A test notebook is provided (`Notebook_profile_test.ipynb`) against which we can test various elements of this notebook.\n",
"\n",
"### Potential Future Work\n",
"\n",
"In terms of analysing cell outputs (not covered as yet), reports could be generated on the sorts of asset that appear to be displayed in each cell output, whether code warnings or errors are raised, etc. There is also potential for running in association with something like [`nbval`](https://github.com/computationalmodelling/nbval) to test that notebooks test correctly against previously run cell outputs.\n",
"\n",
"We might also explore the extent to which interactive notebook profiling tools, such as magics or notebook extensions, be used to support the authoring of new instructional notebooks.\n",
"\n",
"We might also ask to what extent might interactive notebook profiling tools be used to support learners working through instructional material and reflecting on their work? Code health metrics, such as [cell execution success](https://nbgallery.github.io/health_paper.html) used by *nbgallery* may provide clues regarding which code activity cells students struggled to get working, for example. By looking at statistics across students (for example, in assessment notebooks with cell execution success log monitoring enabled) we may be able to identify \"healthy\" or \"unhealthy\" activities; for example, a healthy activity is one in which students can get their code to run with one or two tries, an unhealthy activity is one where they make repeated attempts at trying to get the code to work as they desire. \n",
"\n",
"The notebook profiler should also be runnable against notebooks created using Jupytext from markdown rendered from OU-XML. It would probably make *more* sense to build a custom OU-XML profiler, eg one that could perhaps draw on a summary XML doc generated from OU-XML source docs using XSLT. I'll try to bear in mind creating reporting functions that might be useable in this wider sense. (OU-XML will also have thngs like a/v components, and may have explicit time guidance on expected time spent on particular activities.)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Settings\n",
"\n",
"The following parameters are used notebook wide in the generation of reports."
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"READING_RATE = 100 # words per minute\n",
"# What is a sensible reading rate for undergraduate level academic teaching material?\n",
"# 250 wpm gives a rate of 15,000 wph\n",
"# 10,000 wph corresponds to about 170 words per minute\n",
"# OU guidance: 35 wpm for challenging texts, 70 wpm for medium texts, 120 wpm for easy texts\n",
"\n",
"CODE_READING_RATE = 35 # tokens per minute -- UNUSED\n",
"\n",
"CODE_LINE_READING_TIME = 1 # time in seconds to read a code line\n",
"\n",
"LINE_WIDTH = 160 #character width of a line of markdown text; used to calculate \"screen lines\"\n",
"\n",
"CODE_CELL_REVIEW_TIME = 5 # nominal time in seconds to run each code cell / review each code cell output\n",
"\n",
"CELL_SKIP_TIME = 1 # nomimal time in seconds to move from one cell to the next"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Open Notebook\n",
"\n",
"Open and read a notebook, such as the associated test notebook:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"TEST_NOTEBOOK = 'Notebook_profile_test.ipynb'\n",
"\n",
"import nbformat\n",
"with open(TEST_NOTEBOOK,'r') as f:\n",
" nb = nbformat.reads(f.read(), as_version=4)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Analyse Markdown Cells\n",
"\n",
"Iterate through markdown cells and generate cell by cell reports.\n",
"\n",
"We can start off by generating some simple counts for a single notebook.\n",
"\n",
"Let's preview the contents of a single cell:"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'cell_type': 'markdown',\n",
" 'metadata': {},\n",
" 'source': '# Test Notebook for Notebook Profiler\\n\\nThis notebook provides a test case for the notebook profiler.\\n\\nIt includes a range of markdown and code cells intended to test various features of the profiler.\\n\\nNote that this notebook does not necessarily run...'}"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"nb.cells[0]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can look at just the markdown component associated with a markdown cell - this will be the basis for our markdown text analysis."
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"txt = nb.cells[0]['source']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Estimates of reading time are often based on word count estimates. The Medium website use a reading time estimator that also factors in the presence of images in a text as well as wordcount / sentence length. The [`readtime`](https://github.com/alanhamlett/readtime) package uses the Medium reading time estimation algorithm to give a reading time estimate.\n",
"\n",
"\n",
"?? TO DO - more on the reading time equation; also need something like maybe: +10s for every code cell to run it and look at output? Different reading time per line of code?\n",
"\n",
"*It might be worth looking at forking this reading time estimator and try to factor in reading time elements that reflect the presence of code? Or maybe use a slower reading rate for code? Or factor in code complexity? The presence of links might also affect reading time.*"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'Reading time in seconds: 25.0; in minutes: 1.'"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#https://github.com/alanhamlett/readtime\n",
"#%pip install readtime\n",
"\n",
"import readtime\n",
"import math\n",
"\n",
"rt = readtime.of_markdown(txt, wpm=READING_RATE).delta.total_seconds()\n",
"\n",
"#Round up on the conversion of estimated reading time in seonds, to minutes...\n",
"f'Reading time in seconds: {rt}; in minutes: {math.ceil(rt/60)}.'"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `spacy` natural language processing package provides a wide ranging of basic tools for parsing texts."
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [],
"source": [
"#%pip install spacy\n",
"import spacy\n",
"\n",
"#Check we have the small English model at least\n",
"SPACY_LANG_MODEL = 'en_core_web_sm'\n",
"\n",
"try:\n",
" import en_core_web_sm\n",
"except:\n",
" import spacy.cli\n",
" spacy.cli.download(SPACY_LANG_MODEL)\n",
"\n",
"#Load a model that a text is parsed against\n",
"nlp = spacy.load(SPACY_LANG_MODEL)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To call on `spacy`, we need to create tokenised document representation of the text (conveniently, the original text version is also stored as part of the object)."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"run_control": {
"marked": false
}
},
"outputs": [],
"source": [
"doc = nlp(txt)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `textacy` package builds on `spacy` to provide a range of higher level tools and statistics, from simple statistics such as word and sentence counts to more complex readability scores using a variety of [readability measures](https://readable.com/blog/the-flesch-reading-ease-and-flesch-kincaid-grade-level/).\n",
"\n",
"One way of using readability measures would be to set reading rates dynamically for each markdown cell based on calculated readability scores."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"({'n_sents': 4,\n",
" 'n_words': 40,\n",
" 'n_chars': 203,\n",
" 'n_syllables': 63,\n",
" 'n_unique_words': 27,\n",
" 'n_long_words': 15,\n",
" 'n_monosyllable_words': 25,\n",
" 'n_polysyllable_words': 6},\n",
" {'flesch_kincaid_grade_level': 6.895,\n",
" 'flesch_reading_ease': 63.440000000000026,\n",
" 'smog_index': 10.125756701596842,\n",
" 'gunning_fog_index': 10.0,\n",
" 'coleman_liau_index': 11.080711825000005,\n",
" 'automated_readability_index': 7.47325,\n",
" 'lix': 47.5,\n",
" 'gulpease_index': 68.25,\n",
" 'wiener_sachtextformel': 6.5195})"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#%pip install textacy\n",
"from textacy import TextStats\n",
"\n",
"ts = TextStats(doc)\n",
"ts.basic_counts, ts.readability_stats"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `textacy` package can also pull out notable features in a text, such as key terms or acronyms, both of which may be useful as part of a notebook summary."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[('notebook profiler', 0.08196495093971548),\n",
" ('test case', 0.06744856661263204),\n",
" ('Test Notebook', 0.06479107591582292),\n",
" ('code cell', 0.05486312750180375),\n",
" ('markdown', 0.024974748258550644),\n",
" ('feature', 0.023809657889882128),\n",
" ('range', 0.022746625242650347)]"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#Extract keyterms\n",
"import textacy.ke\n",
"textacy.ke.textrank(doc, normalize=\"lemma\", topn=10)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{}"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from textacy.extract import acronyms_and_definitions\n",
"acronyms_and_definitions(doc)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As well as using measures provided by off-the-shelf packages, it's also useful to define some simple metrics of our own that don't appear in other packages."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To start with, let's try to estimate the notebook length as it appears on screen by calculating how many \"screen lines\" a markdown cell is likely to take up. This can be calculated by splitting long lines of text over multiple lines based on a screen line width parameter."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"import textwrap\n",
"\n",
"def _count_screen_lines(txt, width=LINE_WIDTH):\n",
" \"\"\"Count the number of screen lines that a markdown cell takes up.\"\"\"\n",
" ll = txt.split('\\n\\n')\n",
" _ll = []\n",
" for l in ll:\n",
" #Model screen flow: split a line if it is more than `width` characters long\n",
" _ll=_ll+textwrap.wrap(l, width)\n",
" n_screen_lines = len(_ll)\n",
" return n_screen_lines"
]
},
{
"cell_type": "code",
"execution_count": 81,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"2"
]
},
"execution_count": 81,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"screen_txt='As well as \"text\", markdown cells may contain cell blocks. The following is a basic report generator for summarising key statistical properties of code blocks. (We will see later an alternative way of calculating such metrics for well form Python code at least.)'\n",
"_count_screen_lines(screen_txt)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `textacy` package does not appear to provide average sentence length statistics (although sentence length metrics may play a role in calculating readability scores? So maybe there are usable functions somewhere in there?) but we can straightforwardly define our own."
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"import statistics\n",
"\n",
"def sentence_lengths(doc):\n",
" \"\"\"Generate elementary sentence length statistics.\"\"\"\n",
" s_mean = None\n",
" s_median = None\n",
" s_stdev = None\n",
" s_lengths = []\n",
" for sentence in doc.sents:\n",
" #Punctuation elements are tokens in their own right; remove these from sentence length counts\n",
" s_lengths.append(len( [tok.text for tok in sentence if tok.pos_ != \"PUNCT\"]))\n",
" \n",
" if s_lengths:\n",
" #If we have at least one measure, we can generate some simple statistics\n",
" s_mean = statistics.mean(s_lengths)\n",
" s_median = statistics.median(s_lengths)\n",
" s_stdev = statistics.stdev(s_lengths) if len(s_lengths) > 1 else 0\n",
" \n",
" return s_lengths, s_mean, s_median, s_stdev"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The sentence statistics are generated from a `spacy` `doc` object and returned as separate statistics."
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[7, 11, 18, 8] 11 9.5 4.96655480858378\n"
]
}
],
"source": [
"s_lengths, s_mean, s_median, s_stdev = sentence_lengths(doc)\n",
"print(s_lengths, s_mean, s_median, s_stdev)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As well as \"text\", markdown cells may contain cell blocks. The following is a basic report generator for summarising key statistical propererties of code blocks. (We will see later an alternative way of calculating such metrics for well form Python code at least.)"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [],
"source": [
"def _code_block_summarise(lines,\n",
" n_blank_code_lines = 0,\n",
" n_single_line_comment_code_lines = 0,\n",
" n_code_lines = 0):\n",
" \n",
" lines = lines.splitlines() if isinstance(lines, str) else lines\n",
" \n",
" #if lines[0].startwsith('%%'): \n",
" ##block magic - we could detect which?\n",
" #This would let us report on standard block magic such as %%bash\n",
" #as well as custom magic such as %%sql\n",
" for l in lines:\n",
" if not l.strip():\n",
" n_blank_code_lines = n_blank_code_lines + 1\n",
" elif l.strip().startswith(('#')): #Also pattern match \"\"\".+\"\"\" and '''.+'''\n",
" n_single_line_comment_code_lines = n_single_line_comment_code_lines + 1\n",
" #How should we detect block comments?\n",
" #elif l.strip().startswith(('!')):\n",
" ## IPyhton shell command\n",
" #elif l.startswith('%load_ext'):\n",
" ##Import some magic - we could detect which?\n",
" else:\n",
" n_code_lines = n_code_lines + 1\n",
" return n_blank_code_lines, n_single_line_comment_code_lines, n_code_lines"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can use the code block summary in a more general report on \"features\" within a markdown cell (sentence statistics are handled elsewhere):"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"def _report_md_features(txt):\n",
" \"\"\"Report on features in markdown documents.\n",
" For example, number of headings or paragraphs, or code block analysis.\"\"\"\n",
" n_headers = 0\n",
" n_paras = 0\n",
" n_total_code_lines = 0\n",
" n_code_lines = 0\n",
" n_blank_code_lines = 0\n",
" n_single_line_comment_code_lines = 0\n",
"\n",
" in_code_block = False\n",
" \n",
" n_screen_lines = _count_screen_lines(txt)\n",
" \n",
" #Markdown processor ignores whitespace at start and end of a markdown cell\n",
" txt = txt.strip()\n",
" \n",
" n_code_blocks = 0\n",
" \n",
" #We will see how to improve the handling of code blocks in markdown cells later\n",
" for l in txt.split('\\n'):\n",
" if l.strip().startswith('```'):\n",
" in_code_block = not in_code_block\n",
" if in_code_block:\n",
" n_code_blocks = n_code_blocks + 1\n",
" elif in_code_block:\n",
" n_total_code_lines = n_total_code_lines + 1\n",
" n_blank_code_lines, n_single_line_comment_code_lines, \\\n",
" n_code_lines = _code_block_summarise(l,\n",
" n_blank_code_lines,\n",
" n_single_line_comment_code_lines,\n",
" n_code_lines)\n",
" elif l.startswith('#'):\n",
" #Markdown heading\n",
" n_headers = n_headers + 1\n",
" elif not l.strip():\n",
" #A paragraph is identified by an double end of line (\\n\\n) outside a code block\n",
" #So if we have an empty line that signifies a paragraph break?\n",
" n_paras = n_paras + 1\n",
" \n",
" n_code = (n_total_code_lines, n_code_lines, \\\n",
" n_blank_code_lines, n_single_line_comment_code_lines)\n",
" \n",
" return n_headers, n_paras, n_screen_lines, n_code_blocks, n_code"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So for example, the features we can report on might include the number of headings paragraphs, screen lines, or code block features."
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(1, 3, 4, 0, (0, 0, 0, 0))"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"n_headers, n_paras, n_screen_lines, n_code_blocks, n_code = _report_md_features(txt)\n",
"n_headers, n_paras, n_screen_lines, n_code_blocks, n_code"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(0, 0, 0, 0)"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(n_total_code_lines, n_code_lines, n_blank_code_lines, n_single_line_comment_code_lines) = n_code\n",
"n_total_code_lines, n_code_lines, n_blank_code_lines, n_single_line_comment_code_lines"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Code Blocks in Markdown Cells\n",
"A question arises when we have code blocks appearing in markdown cells. How should these be treated? Should we report the code toward markdown counts, or should we separately treat the code, discounting it from markdown word counts but reporting it as \"code in markdown\"?\n",
"\n",
"Another approach might be to include and codes of block appearing in markdown cells as part of the markdown word count, but provide an additional report identifying how many lines of code appeared as part of the markdown."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `excode` package provides an easy way of grabbing code blocks from markdown text, so we might be able to use that to mprove the handling of code blocks inside markdown cells.\n",
"\n",
"Lets grab a simple text case of some markdown containing some code blocks:"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"This cell contains two code blocks.\n",
"\n",
"Here's one:\n",
"\n",
"```python\n",
"import pandas\n",
"\n",
"#Create a dataframe\n",
"df = pd.DataFrame()\n",
"```\n",
"\n",
"and here's another:\n",
"\n",
"```python\n",
"import pandas\n",
"\n",
"#Create a dataframe\n",
"df = pd.DataFrame()\n",
"```\n",
"\n",
"So that's two...\n"
]
}
],
"source": [
"mc = nb.cells[2]['source']\n",
"print(mc)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's see if we can extract those code blocks..."
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['import pandas\\n\\n#Create a dataframe\\ndf = pd.DataFrame()\\n',\n",
" 'import pandas\\n\\n#Create a dataframe\\ndf = pd.DataFrame()\\n']"
]
},
"execution_count": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#%pip install excode\n",
"import excode\n",
"import io\n",
"\n",
"#excode seems to expect a file buffer...\n",
"excode.extract(io.StringIO(mc))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now report on the structure of code blocks in markdown cells more directly:"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {},
"outputs": [],
"source": [
"def code_block_report(c):\n",
" \"\"\"Generate simple code report when passed a list of code lines\n",
" or a string containing multiple `\\n` separated code lines.\"\"\"\n",
" \n",
" n_total_code_lines = 0\n",
" n_code_lines = 0\n",
" n_blank_code_lines = 0\n",
" n_single_line_comment_code_lines = 0\n",
" \n",
" #We won't count leading or lagging empty lines as code lines...\n",
" lines = c.strip().splitlines() if isinstance(c, str) else c\n",
" \n",
" #If first or last line is empty, strip it\n",
" if len(lines) > 1:\n",
" lines = lines[1:] if not lines[0].strip() else lines\n",
" lines = lines[:-1] if not lines[-1].strip() else lines\n",
" \n",
" #print(lines)\n",
" \n",
" n_total_code_lines = len(lines)\n",
" \n",
" n_blank_code_lines, n_single_line_comment_code_lines, \\\n",
" n_code_lines = _code_block_summarise(lines,\n",
" n_blank_code_lines,\n",
" n_single_line_comment_code_lines,\n",
" n_code_lines)\n",
" \n",
" return (n_total_code_lines, n_blank_code_lines,\\\n",
" n_single_line_comment_code_lines, n_code_lines)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running the above function should generate some simple code statistics:"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"This cell contains two code blocks.\n",
"\n",
"Here's one:\n",
"\n",
"```python\n",
"import pandas\n",
"\n",
"#Create a dataframe\n",
"df = pd.DataFrame()\n",
"```\n",
"\n",
"and here's another:\n",
"\n",
"```python\n",
"import pandas\n",
"\n",
"#Create a dataframe\n",
"df = pd.DataFrame()\n",
"```\n",
"\n",
"So that's two...\n",
"4 1 1 2\n",
"4 1 1 2\n"
]
}
],
"source": [
"print(mc)\n",
"for c in excode.extract(io.StringIO(mc)):\n",
" (n_total_code_lines, n_blank_code_lines, \\\n",
" n_single_line_comment_code_lines, n_code_lines) = code_block_report(c)\n",
" \n",
" print(n_total_code_lines, n_blank_code_lines, \\\n",
" n_single_line_comment_code_lines, n_code_lines )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We could also use the `radon` code analyser (which does count empty lines as code lines unless we explictly strip them).\n",
"\n",
"However, it should be noted that the `radon` code analysis relies on well formed Python code that can be loaded as into the Python AST parser. This means that code that doesn't parse as valid Python, either because it contains an error or because the code is not actually Python code (for example, in course materials we make use of SQL block magic to allow us to write SQL code in a code cell).\n",
"\n",
"The `radon` parser will also report an error if it comes across IPython line or cell magic code, or `!` prefixed shell commands.\n",
"\n",
"We will see later how we can start to cleanse a code string of IPython `!` and `%` prefixed directives when we consider parsing code cells. "
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Module(loc=4, lloc=2, sloc=2, comments=1, multi=0, blank=1, single_comments=1)\n",
"Module(loc=4, lloc=2, sloc=2, comments=1, multi=0, blank=1, single_comments=1)\n"
]
},
{
"data": {
"text/plain": [
"(4, 2, 2, 1, 0, 1, 1)"
]
},
"execution_count": 23,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#%pip install radon\n",
"from radon.raw import analyze\n",
"for c in excode.extract(io.StringIO(mc)):\n",
" r = analyze(c.strip())\n",
" print(r)\n",
"r.loc, r.lloc, r.sloc, r.comments, r.multi, r.blank, r.single_comments"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can bundle up the `radon` analyzer to make it a little easier to call for our purposes:"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [],
"source": [
"def r_analyze(c):\n",
" \"\"\"Analyse a code string using radon.analyze.\"\"\"\n",
" r = analyze(c.strip())\n",
" n_total_code_lines = r.loc\n",
" n_blank_code_lines = r.blank\n",
" n_single_line_comment_code_lines = r.comments\n",
" n_code_lines = r.sloc\n",
" return (n_total_code_lines, n_blank_code_lines, \\\n",
" n_single_line_comment_code_lines, n_code_lines)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can then siple call `r_analyze()` function with a code string:"
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"4 1 1 2\n",
"4 1 1 2\n"
]
}
],
"source": [
"for c in excode.extract(io.StringIO(mc)):\n",
" (n_total_code_lines, n_blank_code_lines, \\\n",
" n_single_line_comment_code_lines, n_code_lines) = r_analyze(c)\n",
" \n",
" print(n_total_code_lines, n_blank_code_lines, \\\n",
" n_single_line_comment_code_lines, n_code_lines)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Code Reading (and Execution) Time\n",
"\n",
"It would be useful if we had a heuristic for code reading time.\n",
"\n",
"One approach would be to tokenise the code and estimate reading time from a simple \"tokens per minute\" reading rate, or use a reading rate appropriate for \"difficult\" text. Another approach might be to try to make use of code complexity scores and code length.\n",
"\n",
"A pragmatic way may just be to estimate based on lines of code, with a nominal reading time allocated to each line of code."
]
},
{
"cell_type": "code",
"execution_count": 26,
"metadata": {},
"outputs": [],
"source": [
"def code_reading_time(n_code_lines, n_single_line_comment_code_lines, line_time=CODE_LINE_READING_TIME):\n",
" \"\"\"Crude reading time estimate for a code block.\"\"\"\n",
" code_reading_time = line_time * (n_code_lines + n_single_line_comment_code_lines)\n",
" return code_reading_time"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The way we currently process code in markdown cells, it will be timed at the standard reading rate. It may be appropriate to add a simple modifier that also adds a \"code reading overhead\" to the reading time based on the amount of code in a markdown cell.\n",
"\n",
"For code in code cells, rather than code blocks in markdown cells, might also be worth exploring *code execution time*, that is, an overhead associated with running each code cell. A crude way of calculating this would be to levy a fixed amount of time to account for running the code cell and inspecting the result. A more considered approach would look to cell profiling / execution time logs and code cell outputs in a run notebook."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Custom Report Aggregator\n",
"\n",
"For convenience, we can bundle up the custom metrics we have created into a function that returns a single report object."
]
},
{
"cell_type": "code",
"execution_count": 27,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"def process_extras(doc):\n",
" \"\"\"Generate a dict containing additional metrics.\"\"\"\n",
" \n",
" n_headers, n_paras, n_screen_lines, n_code_blocks, n_code = _report_md_features(doc.text)\n",
" s_lengths, s_mean, s_median, s_stdev = sentence_lengths(doc)\n",
" (n_total_code_lines, n_code_lines, n_blank_code_lines, n_single_line_comment_code_lines) = n_code\n",
" \n",
" _reading_time = readtime.of_markdown(doc.text, wpm=READING_RATE).delta.total_seconds()\n",
" #Add reading time overhead for code\n",
" line_of_code_overhead = 1 #time in seconds to add to reading of each code line\n",
" _reading_time = _reading_time + code_reading_time(n_code_lines, n_single_line_comment_code_lines,\n",
" line_of_code_overhead)\n",
" \n",
" extras = {'n_headers':n_headers,\n",
" 'n_paras':n_paras,\n",
" 'n_screen_lines':n_screen_lines,\n",
" 's_lengths':s_lengths,\n",
" 's_mean':s_mean,\n",
" 's_median':s_median,\n",
" 's_stdev':s_stdev,\n",
" 'n_code_blocks':n_code_blocks,\n",
" 'n_total_code_lines':n_total_code_lines,\n",
" 'n_code_lines':n_code_lines,\n",
" 'n_blank_code_lines':n_blank_code_lines,\n",
" 'n_single_line_comment_code_lines':n_single_line_comment_code_lines,\n",
" 'reading_time_s':_reading_time,\n",
" 'reading_time_mins': math.ceil(_reading_time/60),\n",
" 'mean_sentence_length': s_mean,\n",
" 'median_sentence_length': s_median,\n",
" 'stdev_sentence_length': s_stdev,\n",
" #The following are both listy, so we need to handle them when we move to a dataframe\n",
" # TO DO - paramterise the number of key terms\n",
" 'keyterms':textacy.ke.textrank(doc, normalize=\"lemma\", topn=10),\n",
" 'acronyms':acronyms_and_definitions(doc)\n",
" }\n",
" return extras"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running the `process_extras()` function on a `doc` object returns the extra metrics as keyed items in a single `dict`:"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"({'n_sents': 4,\n",
" 'n_words': 40,\n",
" 'n_chars': 203,\n",
" 'n_syllables': 63,\n",
" 'n_unique_words': 27,\n",
" 'n_long_words': 15,\n",
" 'n_monosyllable_words': 25,\n",
" 'n_polysyllable_words': 6},\n",
" {'flesch_kincaid_grade_level': 6.895,\n",
" 'flesch_reading_ease': 63.440000000000026,\n",
" 'smog_index': 10.125756701596842,\n",
" 'gunning_fog_index': 10.0,\n",
" 'coleman_liau_index': 11.080711825000005,\n",
" 'automated_readability_index': 7.47325,\n",
" 'lix': 47.5,\n",
" 'gulpease_index': 68.25,\n",
" 'wiener_sachtextformel': 6.5195},\n",
" {'n_headers': 1,\n",
" 'n_paras': 3,\n",
" 'n_screen_lines': 4,\n",
" 's_lengths': [7, 11, 18, 8],\n",
" 's_mean': 11,\n",
" 's_median': 9.5,\n",
" 's_stdev': 4.96655480858378,\n",
" 'n_code_blocks': 0,\n",
" 'n_total_code_lines': 0,\n",
" 'n_code_lines': 0,\n",
" 'n_blank_code_lines': 0,\n",
" 'n_single_line_comment_code_lines': 0,\n",
" 'reading_time_s': 25.0,\n",
" 'reading_time_mins': 1,\n",
" 'mean_sentence_length': 11,\n",
" 'median_sentence_length': 9.5,\n",
" 'stdev_sentence_length': 4.96655480858378,\n",
" 'keyterms': [('notebook profiler', 0.08196495093971548),\n",
" ('test case', 0.06744856661263204),\n",
" ('Test Notebook', 0.06479107591582292),\n",
" ('code cell', 0.05486312750180375),\n",
" ('markdown', 0.024974748258550644),\n",
" ('feature', 0.023809657889882128),\n",
" ('range', 0.022746625242650347)],\n",
" 'acronyms': {}})"
]
},
"execution_count": 28,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ts.basic_counts, ts.readability_stats, process_extras(doc)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Generate a Whole Notebook Markdown Report\n",
"\n",
"The whole notebook report can come in various flavours:\n",
" \n",
"- top level summary statistics that merge all the markdown content into a single cell and then analyse that;\n",
"- aggregated cell level statistics that summarise the statistics calculated for each markdown cell separately;\n",
"- individual cell level statistics that report the statistics for each cell separately.\n",
"\n",
"Whilst the individual cell level statistics presented in a textual fashion may be overkill, it may be useful to generate visual displays of a notebook that graphically summarise its structure."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Top-Level Summary\n",
"\n",
"Let's start with a report that munges the all the markdown text together and report on that..."
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {},
"outputs": [],
"source": [
"def process_notebook_full_md(nb):\n",
" \"\"\"Given a notebook, return all the markdown cell content as one string,\n",
" and all the code cell content as another string.\"\"\"\n",
" txt = []\n",
" code = []\n",
" for cell in nb.cells:\n",
" if cell['cell_type']=='markdown':\n",
" txt.append(cell['source'])\n",
" elif cell['cell_type']=='code':\n",
" code.append( cell['source'])\n",
"\n",
" doc = nlp('\\n\\n'.join(txt))\n",
" code = '\\n\\n'.join(code)\n",
" \n",
" return doc, code"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `process_notebook_full_md()` function takes a notebook object and returns two strings: one containing all the notebook's markdown cell content, one containing all its code cell content."
]
},
{
"cell_type": "code",
"execution_count": 30,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"('# Test Notebook for Notebook Profiler\\n\\nThis notebook provides a test case for the notebook profiler.\\n\\nIt includes a range of markdown and code cells intended to test various features of the profiler.\\n\\nNote that this notebook does not necessarily run...\\n\\n## Markdown Cells With Cod',\n",
" '# This is a code cell\\nimport pandas\\n\\n#Create a dataframe\\ndf = pd.DataFrame()\\n\\n# This is a code cell with a magic...\\n\\n%matplotlib inline\\nimport time\\n\\ndef fn():\\n \"\"\"How is the docstring handled?\"\"\"\\n pass\\n\\n%load_ext sql\\n\\n%%sql\\nSELECT * FROM TABLE;')"
]
},
"execution_count": 30,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"full_doc, full_code = process_notebook_full_md(nb)\n",
"full_doc.text[:280], full_code[:250]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's make things a bit more tabular in our reporting:"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"\n",
"def process_notebook_md_doc(doc):\n",
" ts = TextStats(doc)\n",
" return pd.DataFrame([{'text':doc.text,\n",
" **ts.basic_counts, **ts.readability_stats, **process_extras(doc)}])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Running the `process_notebook_md_doc()` function on a `doc` object returns a single row dataframe containing summary statistics calculated over the full markdown content of the notebook."
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>text</th>\n",
" <th>n_sents</th>\n",
" <th>n_words</th>\n",
" <th>n_chars</th>\n",
" <th>n_syllables</th>\n",
" <th>n_unique_words</th>\n",
" <th>n_long_words</th>\n",
" <th>n_monosyllable_words</th>\n",
" <th>n_polysyllable_words</th>\n",
" <th>flesch_kincaid_grade_level</th>\n",
" <th>...</th>\n",
" <th>n_code_lines</th>\n",
" <th>n_blank_code_lines</th>\n",
" <th>n_single_line_comment_code_lines</th>\n",
" <th>reading_time_s</th>\n",
" <th>reading_time_mins</th>\n",
" <th>mean_sentence_length</th>\n",
" <th>median_sentence_length</th>\n",
" <th>stdev_sentence_length</th>\n",
" <th>keyterms</th>\n",
" <th>acronyms</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td># Test Notebook for Notebook Profiler\\n\\nThis ...</td>\n",
" <td>15</td>\n",
" <td>119</td>\n",
" <td>499</td>\n",
" <td>159</td>\n",
" <td>49</td>\n",
" <td>26</td>\n",
" <td>89</td>\n",
" <td>8</td>\n",
" <td>3.270387</td>\n",
" <td>...</td>\n",
" <td>6</td>\n",
" <td>0</td>\n",
" <td>3</td>\n",
" <td>69.0</td>\n",
" <td>2</td>\n",
" <td>8.733333</td>\n",
" <td>8</td>\n",
" <td>5.417784</td>\n",
" <td>[(single code block, 0.05399890062211835), (co...</td>\n",
" <td>{}</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>1 rows × 37 columns</p>\n",
"</div>"
],
"text/plain": [
" text n_sents n_words \\\n",
"0 # Test Notebook for Notebook Profiler\\n\\nThis ... 15 119 \n",
"\n",
" n_chars n_syllables n_unique_words n_long_words n_monosyllable_words \\\n",
"0 499 159 49 26 89 \n",
"\n",
" n_polysyllable_words flesch_kincaid_grade_level ... n_code_lines \\\n",
"0 8 3.270387 ... 6 \n",
"\n",
" n_blank_code_lines n_single_line_comment_code_lines reading_time_s \\\n",
"0 0 3 69.0 \n",
"\n",
" reading_time_mins mean_sentence_length median_sentence_length \\\n",
"0 2 8.733333 8 \n",
"\n",
" stdev_sentence_length keyterms \\\n",
"0 5.417784 [(single code block, 0.05399890062211835), (co... \n",
"\n",
" acronyms \n",
"0 {} \n",
"\n",
"[1 rows x 37 columns]"
]
},
"execution_count": 32,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"process_notebook_md_doc(full_doc)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"#### Summarised Cell Level Reporting\n",
"\n",
"For the summarised cell level reporting, generate measures on a per cell basis and then calculate summary statistics over those."
]
},
{
"cell_type": "code",
"execution_count": 72,
"metadata": {},
"outputs": [],
"source": [
"def process_notebook_md(nb, fn=''):\n",
" \"\"\"Process all the markdown cells in a notebook.\"\"\"\n",
" cell_reports = pd.DataFrame()\n",
" \n",
" for i, cell in enumerate(nb.cells):\n",
" if cell['cell_type']=='markdown':\n",
" _metrics = process_notebook_md_doc( nlp( cell['source'] ))\n",
" _metrics['cell_count'] = i\n",
" _metrics['cell_type'] = 'md'\n",
" cell_reports = cell_reports.append(_metrics, sort=False)\n",
" \n",
" cell_reports['filename'] = fn\n",
" cell_reports.reset_index(drop=True, inplace=True)\n",
" return cell_reports"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Processing a single notebook returns a dataframe with one row per markdown cell with each metric reported in its own column."
]
},
{
"cell_type": "code",
"execution_count": 73,
"metadata": {
"scrolled": false
},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>text</th>\n",
" <th>n_sents</th>\n",
" <th>n_words</th>\n",
" <th>n_chars</th>\n",
" <th>n_syllables</th>\n",
" <th>n_unique_words</th>\n",
" <th>n_long_words</th>\n",
" <th>n_monosyllable_words</th>\n",
" <th>n_polysyllable_words</th>\n",
" <th>flesch_kincaid_grade_level</th>\n",
" <th>...</th>\n",
" <th>reading_time_s</th>\n",
" <th>reading_time_mins</th>\n",
" <th>mean_sentence_length</th>\n",
" <th>median_sentence_length</th>\n",
" <th>stdev_sentence_length</th>\n",
" <th>keyterms</th>\n",
" <th>acronyms</th>\n",
" <th>cell_count</th>\n",
" <th>cell_type</th>\n",
" <th>filename</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td># Test Notebook for Notebook Profiler\\n\\nThis ...</td>\n",
" <td>4</td>\n",
" <td>40</td>\n",
" <td>203</td>\n",
" <td>63</td>\n",
" <td>27</td>\n",
" <td>15</td>\n",
" <td>25</td>\n",
" <td>6</td>\n",
" <td>6.895000</td>\n",
" <td>...</td>\n",
" <td>25.0</td>\n",
" <td>1</td>\n",
" <td>11.00</td>\n",
" <td>9.5</td>\n",
" <td>4.966555</td>\n",
" <td>[(notebook profiler, 0.08196495093971548), (te...</td>\n",
" <td>{}</td>\n",
" <td>0</td>\n",
" <td>md</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>## Markdown Cells With Code Blocks\\n\\nThis cel...</td>\n",
" <td>4</td>\n",
" <td>30</td>\n",
" <td>123</td>\n",
" <td>38</td>\n",
" <td>22</td>\n",
" <td>5</td>\n",
" <td>23</td>\n",
" <td>1</td>\n",
" <td>2.281667</td>\n",
" <td>...</td>\n",
" <td>18.0</td>\n",
" <td>1</td>\n",
" <td>8.25</td>\n",
" <td>5.5</td>\n",
" <td>8.261356</td>\n",
" <td>[(single code block, 0.09825762538579677), (Ma...</td>\n",
" <td>{}</td>\n",
" <td>1</td>\n",
" <td>md</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>This cell contains two code blocks.\\n\\nHere's ...</td>\n",
" <td>8</td>\n",
" <td>49</td>\n",
" <td>173</td>\n",
" <td>58</td>\n",
" <td>23</td>\n",
" <td>6</td>\n",
" <td>41</td>\n",
" <td>1</td>\n",
" <td>0.766097</td>\n",
" <td>...</td>\n",
" <td>28.0</td>\n",
" <td>1</td>\n",
" <td>6.50</td>\n",
" <td>6.0</td>\n",
" <td>4.105745</td>\n",
" <td>[(code block, 0.052250174985765105), (import p...</td>\n",
" <td>{}</td>\n",
" <td>2</td>\n",
" <td>md</td>\n",
" <td></td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>3 rows × 40 columns</p>\n",
"</div>"
],
"text/plain": [
" text n_sents n_words \\\n",
"0 # Test Notebook for Notebook Profiler\\n\\nThis ... 4 40 \n",
"1 ## Markdown Cells With Code Blocks\\n\\nThis cel... 4 30 \n",
"2 This cell contains two code blocks.\\n\\nHere's ... 8 49 \n",
"\n",
" n_chars n_syllables n_unique_words n_long_words n_monosyllable_words \\\n",
"0 203 63 27 15 25 \n",
"1 123 38 22 5 23 \n",
"2 173 58 23 6 41 \n",
"\n",
" n_polysyllable_words flesch_kincaid_grade_level ... reading_time_s \\\n",
"0 6 6.895000 ... 25.0 \n",
"1 1 2.281667 ... 18.0 \n",
"2 1 0.766097 ... 28.0 \n",
"\n",
" reading_time_mins mean_sentence_length median_sentence_length \\\n",
"0 1 11.00 9.5 \n",
"1 1 8.25 5.5 \n",
"2 1 6.50 6.0 \n",
"\n",
" stdev_sentence_length keyterms \\\n",
"0 4.966555 [(notebook profiler, 0.08196495093971548), (te... \n",
"1 8.261356 [(single code block, 0.09825762538579677), (Ma... \n",
"2 4.105745 [(code block, 0.052250174985765105), (import p... \n",
"\n",
" acronyms cell_count cell_type filename \n",
"0 {} 0 md \n",
"1 {} 1 md \n",
"2 {} 2 md \n",
"\n",
"[3 rows x 40 columns]"
]
},
"execution_count": 73,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"total_report = process_notebook_md(nb)\n",
"total_report.head(3)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"It is trivial to create summary statistics directly from the *per* cell report table by aggregating over rows associated with the same notebook; in this case, we can find the total readtime as a simple sum.\n",
"\n",
"However, more generally we may wish to apply the aggegation over a set of grouped results (for example, in a dataframe containing materics from mutliple notebooks, we would want to group by each notebook and then perform the agggragatin on the measures associated with each notebook)."
]
},
{
"cell_type": "code",
"execution_count": 35,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"3"
]
},
"execution_count": 35,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"total_report['reading_time_mins'].sum()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's also create a function to profile a notebook from a file:"
]
},
{
"cell_type": "code",
"execution_count": 36,
"metadata": {},
"outputs": [],
"source": [
"def process_notebook_file(fn):\n",
" \"\"\"Grab cell level statistics across a whole notebook.\"\"\"\n",
" \n",
" with open(fn,'r') as f:\n",
" try:\n",
" nb = nbformat.reads(f.read(), as_version=4)\n",
" cell_reports = process_notebook_md(nb, fn=fn)\n",
" except:\n",
" print(f'FAILED to process {fn}')\n",
" cell_reports = pd.DataFrame()\n",
" \n",
" cell_reports.reset_index(drop=True, inplace=True)\n",
" return cell_reports"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `process_notbook_file()` function returns a dataframe containing row level reports for each markdown cell in a specified notebook:"
]
},
{
"cell_type": "code",
"execution_count": 37,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>text</th>\n",
" <th>n_sents</th>\n",
" <th>n_words</th>\n",
" <th>n_chars</th>\n",
" <th>n_syllables</th>\n",
" <th>n_unique_words</th>\n",
" <th>n_long_words</th>\n",
" <th>n_monosyllable_words</th>\n",
" <th>n_polysyllable_words</th>\n",
" <th>flesch_kincaid_grade_level</th>\n",
" <th>...</th>\n",
" <th>reading_time_s</th>\n",
" <th>reading_time_mins</th>\n",
" <th>mean_sentence_length</th>\n",
" <th>median_sentence_length</th>\n",
" <th>stdev_sentence_length</th>\n",
" <th>keyterms</th>\n",
" <th>acronyms</th>\n",
" <th>cell_count</th>\n",
" <th>cell_type</th>\n",
" <th>filename</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td># Test Notebook for Notebook Profiler\\n\\nThis ...</td>\n",
" <td>4</td>\n",
" <td>40</td>\n",
" <td>203</td>\n",
" <td>63</td>\n",
" <td>27</td>\n",
" <td>15</td>\n",
" <td>25</td>\n",
" <td>6</td>\n",
" <td>6.895000</td>\n",
" <td>...</td>\n",
" <td>25.0</td>\n",
" <td>1</td>\n",
" <td>11.00</td>\n",
" <td>9.5</td>\n",
" <td>4.966555</td>\n",
" <td>[(notebook profiler, 0.08196495093971548), (te...</td>\n",
" <td>{}</td>\n",
" <td>0</td>\n",
" <td>md</td>\n",
" <td>Notebook_profile_test.ipynb</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>## Markdown Cells With Code Blocks\\n\\nThis cel...</td>\n",
" <td>4</td>\n",
" <td>30</td>\n",
" <td>123</td>\n",
" <td>38</td>\n",
" <td>22</td>\n",
" <td>5</td>\n",
" <td>23</td>\n",
" <td>1</td>\n",
" <td>2.281667</td>\n",
" <td>...</td>\n",
" <td>18.0</td>\n",
" <td>1</td>\n",
" <td>8.25</td>\n",
" <td>5.5</td>\n",
" <td>8.261356</td>\n",
" <td>[(single code block, 0.09825762538579677), (Ma...</td>\n",
" <td>{}</td>\n",
" <td>1</td>\n",
" <td>md</td>\n",
" <td>Notebook_profile_test.ipynb</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>This cell contains two code blocks.\\n\\nHere's ...</td>\n",
" <td>8</td>\n",
" <td>49</td>\n",
" <td>173</td>\n",
" <td>58</td>\n",
" <td>23</td>\n",
" <td>6</td>\n",
" <td>41</td>\n",
" <td>1</td>\n",
" <td>0.766097</td>\n",
" <td>...</td>\n",
" <td>28.0</td>\n",
" <td>1</td>\n",
" <td>6.50</td>\n",
" <td>6.0</td>\n",
" <td>4.105745</td>\n",
" <td>[(code block, 0.052250174985765105), (import p...</td>\n",
" <td>{}</td>\n",
" <td>2</td>\n",
" <td>md</td>\n",
" <td>Notebook_profile_test.ipynb</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>3 rows × 40 columns</p>\n",
"</div>"
],
"text/plain": [
" text n_sents n_words \\\n",
"0 # Test Notebook for Notebook Profiler\\n\\nThis ... 4 40 \n",
"1 ## Markdown Cells With Code Blocks\\n\\nThis cel... 4 30 \n",
"2 This cell contains two code blocks.\\n\\nHere's ... 8 49 \n",
"\n",
" n_chars n_syllables n_unique_words n_long_words n_monosyllable_words \\\n",
"0 203 63 27 15 25 \n",
"1 123 38 22 5 23 \n",
"2 173 58 23 6 41 \n",
"\n",
" n_polysyllable_words flesch_kincaid_grade_level ... reading_time_s \\\n",
"0 6 6.895000 ... 25.0 \n",
"1 1 2.281667 ... 18.0 \n",
"2 1 0.766097 ... 28.0 \n",
"\n",
" reading_time_mins mean_sentence_length median_sentence_length \\\n",
"0 1 11.00 9.5 \n",
"1 1 8.25 5.5 \n",
"2 1 6.50 6.0 \n",
"\n",
" stdev_sentence_length keyterms \\\n",
"0 4.966555 [(notebook profiler, 0.08196495093971548), (te... \n",
"1 8.261356 [(single code block, 0.09825762538579677), (Ma... \n",
"2 4.105745 [(code block, 0.052250174985765105), (import p... \n",
"\n",
" acronyms cell_count cell_type filename \n",
"0 {} 0 md Notebook_profile_test.ipynb \n",
"1 {} 1 md Notebook_profile_test.ipynb \n",
"2 {} 2 md Notebook_profile_test.ipynb \n",
"\n",
"[3 rows x 40 columns]"
]
},
"execution_count": 37,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"process_notebook_file(TEST_NOTEBOOK)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Analysing Multiple Notebooks in the Same Directory\n",
"\n",
"As well as analysing notebooks at the notebook level, we may also want to generate individual and aggregated reports for all the notebooks contained in a single directory.\n",
"\n",
"Aggregated reports might include the total estimated time to work through all the notebooks in the directory, for example.\n",
"\n",
"It might be useful to have one entry point and a switch that selects between the notebook summary reports and the full cell level report? Or maybe we should report two dataframes always - aggregated notebook level and individual cell level?"
]
},
{
"cell_type": "code",
"execution_count": 38,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"def _nb_dir_file_profiler(path, _f, report=False):\n",
" \"\"\"Get the profile for a single file on a specified path.\"\"\"\n",
" f = os.path.join(path, _f)\n",
" if f.endswith('.ipynb'):\n",
" if report:\n",
" print(f'Profiling {f}')\n",
" return process_notebook_file(f)\n",
" return pd.DataFrame()\n",
" \n",
"def nb_dir_profiler(path):\n",
" \"\"\"Profile all the notebooks in a specific directory.\"\"\"\n",
" nb_dir_report = pd.DataFrame()\n",
" for _f in os.listdir(path):\n",
" nb_dir_report = nb_dir_report.append( _nb_dir_profiler(path, _f), sort=False )\n",
" #nb_dir_report['path'] = path\n",
" return nb_dir_report "
]
},
{
"cell_type": "code",
"execution_count": 39,
"metadata": {},
"outputs": [],
"source": [
"#nb_dir_profiler('.')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Analysing Notebooks Across Multiple Directories\n",
"\n",
"As well as analysing all the notebooks contained within a single directory, we may want to automate the production of reports at the directory level across multiple directories."
]
},
{
"cell_type": "code",
"execution_count": 185,
"metadata": {},
"outputs": [],
"source": [
"def nb_multidir_profiler(path, exclude = 'default'):\n",
" \"\"\"Profile all the notebooks in a specific directory and in any child directories.\"\"\"\n",
" \n",
" if exclude == 'default':\n",
" exclude_paths = ['.ipynb_checkpoints', '.git', '.ipynb', '__MACOSX']\n",
" else:\n",
" #If we set exclude, we need to pass it as a list\n",
" exclude_paths = exclude\n",
" nb_multidir_report = pd.DataFrame()\n",
" for _path, dirs, files in os.walk(path):\n",
" #Start walking...\n",
" #If we're in a directory that is not excluded...\n",
" if not set(exclude_paths).intersection(set(_path.split('/'))):\n",
" #Profile that directory...\n",
" nb_dir_report = pd.DataFrame()\n",
" for _f in files:\n",
" nb_dir_report = nb_dir_report.append( _nb_dir_file_profiler(_path, _f), sort=False )\n",
" if not nb_dir_report.empty:\n",
" nb_dir_report['path'] = _path\n",
" nb_multidir_report = nb_multidir_report.append(nb_dir_report, sort=False)\n",
" \n",
" nb_multidir_report = nb_multidir_report.sort_values(by=['path', 'filename'])\n",
" \n",
" nb_multidir_report.reset_index(drop=True, inplace=True)\n",
" \n",
" return nb_multidir_report "
]
},
{
"cell_type": "code",
"execution_count": 186,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>text</th>\n",
" <th>n_sents</th>\n",
" <th>n_words</th>\n",
" <th>n_chars</th>\n",
" <th>n_syllables</th>\n",
" <th>n_unique_words</th>\n",
" <th>n_long_words</th>\n",
" <th>n_monosyllable_words</th>\n",
" <th>n_polysyllable_words</th>\n",
" <th>flesch_kincaid_grade_level</th>\n",
" <th>...</th>\n",
" <th>reading_time_mins</th>\n",
" <th>mean_sentence_length</th>\n",
" <th>median_sentence_length</th>\n",
" <th>stdev_sentence_length</th>\n",
" <th>keyterms</th>\n",
" <th>acronyms</th>\n",
" <th>cell_count</th>\n",
" <th>cell_type</th>\n",
" <th>filename</th>\n",
" <th>path</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td># The *pandas* library: Series and DataFrames</td>\n",
" <td>1</td>\n",
" <td>6</td>\n",
" <td>35</td>\n",
" <td>9</td>\n",
" <td>6</td>\n",
" <td>2</td>\n",
" <td>3</td>\n",
" <td>0</td>\n",
" <td>4.450000</td>\n",
" <td>...</td>\n",
" <td>1</td>\n",
" <td>7.000000</td>\n",
" <td>7.0</td>\n",
" <td>0.000000</td>\n",
" <td>[(Series, 0.12192605097566381), (library, 0.11...</td>\n",
" <td>{}</td>\n",
" <td>0</td>\n",
" <td>md</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Python is a general-purpose scripting language...</td>\n",
" <td>10</td>\n",
" <td>123</td>\n",
" <td>570</td>\n",
" <td>188</td>\n",
" <td>79</td>\n",
" <td>30</td>\n",
" <td>75</td>\n",
" <td>13</td>\n",
" <td>7.242772</td>\n",
" <td>...</td>\n",
" <td>2</td>\n",
" <td>12.500000</td>\n",
" <td>12.0</td>\n",
" <td>10.058164</td>\n",
" <td>[(level datum structure, 0.050604038106577987)...</td>\n",
" <td>{}</td>\n",
" <td>1</td>\n",
" <td>md</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>Note there are several libraries that we shall...</td>\n",
" <td>3</td>\n",
" <td>64</td>\n",
" <td>338</td>\n",
" <td>92</td>\n",
" <td>49</td>\n",
" <td>17</td>\n",
" <td>45</td>\n",
" <td>5</td>\n",
" <td>9.692500</td>\n",
" <td>...</td>\n",
" <td>1</td>\n",
" <td>22.333333</td>\n",
" <td>26.0</td>\n",
" <td>10.016653</td>\n",
" <td>[(standard Python code base, 0.058213526433021...</td>\n",
" <td>{}</td>\n",
" <td>3</td>\n",
" <td>md</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>## Python recap: lists and dicts</td>\n",
" <td>1</td>\n",
" <td>5</td>\n",
" <td>24</td>\n",
" <td>6</td>\n",
" <td>5</td>\n",
" <td>0</td>\n",
" <td>4</td>\n",
" <td>0</td>\n",
" <td>0.520000</td>\n",
" <td>...</td>\n",
" <td>1</td>\n",
" <td>7.000000</td>\n",
" <td>7.0</td>\n",
" <td>0.000000</td>\n",
" <td>[(Python recap, 0.2923854294015616), (list, 0....</td>\n",
" <td>{}</td>\n",
" <td>4</td>\n",
" <td>md</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>Python lists are flexible, mutable, data struc...</td>\n",
" <td>1</td>\n",
" <td>18</td>\n",
" <td>89</td>\n",
" <td>28</td>\n",
" <td>18</td>\n",
" <td>6</td>\n",
" <td>11</td>\n",
" <td>3</td>\n",
" <td>9.785556</td>\n",
" <td>...</td>\n",
" <td>1</td>\n",
" <td>18.000000</td>\n",
" <td>18.0</td>\n",
" <td>0.000000</td>\n",
" <td>[(python list, 0.12775495473120263), (data str...</td>\n",
" <td>{}</td>\n",
" <td>5</td>\n",
" <td>md</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" <td>../Documents/GitHub/tm351-undercertainty/noteb...</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>5 rows × 41 columns</p>\n",
"</div>"
],
"text/plain": [
" text n_sents n_words \\\n",
"0 # The *pandas* library: Series and DataFrames 1 6 \n",
"1 Python is a general-purpose scripting language... 10 123 \n",
"2 Note there are several libraries that we shall... 3 64 \n",
"3 ## Python recap: lists and dicts 1 5 \n",
"4 Python lists are flexible, mutable, data struc... 1 18 \n",
"\n",
" n_chars n_syllables n_unique_words n_long_words n_monosyllable_words \\\n",
"0 35 9 6 2 3 \n",
"1 570 188 79 30 75 \n",
"2 338 92 49 17 45 \n",
"3 24 6 5 0 4 \n",
"4 89 28 18 6 11 \n",
"\n",
" n_polysyllable_words flesch_kincaid_grade_level ... reading_time_mins \\\n",
"0 0 4.450000 ... 1 \n",
"1 13 7.242772 ... 2 \n",
"2 5 9.692500 ... 1 \n",
"3 0 0.520000 ... 1 \n",
"4 3 9.785556 ... 1 \n",
"\n",
" mean_sentence_length median_sentence_length stdev_sentence_length \\\n",
"0 7.000000 7.0 0.000000 \n",
"1 12.500000 12.0 10.058164 \n",
"2 22.333333 26.0 10.016653 \n",
"3 7.000000 7.0 0.000000 \n",
"4 18.000000 18.0 0.000000 \n",
"\n",
" keyterms acronyms cell_count \\\n",
"0 [(Series, 0.12192605097566381), (library, 0.11... {} 0 \n",
"1 [(level datum structure, 0.050604038106577987)... {} 1 \n",
"2 [(standard Python code base, 0.058213526433021... {} 3 \n",
"3 [(Python recap, 0.2923854294015616), (list, 0.... {} 4 \n",
"4 [(python list, 0.12775495473120263), (data str... {} 5 \n",
"\n",
" cell_type filename \\\n",
"0 md ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"1 md ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"2 md ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"3 md ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"4 md ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"\n",
" path \n",
"0 ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"1 ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"2 ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"3 ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"4 ../Documents/GitHub/tm351-undercertainty/noteb... \n",
"\n",
"[5 rows x 41 columns]"
]
},
"execution_count": 186,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"TEST_DIR = '../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks'\n",
"\n",
"ddf = nb_multidir_profiler(TEST_DIR)\n",
"ddf.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Under the grouped report, we note that the summed reading time in minutes is likely to significantly overestimate the reading time requirement, representing as it does the sum of time in minutes rounded up from seconds. The lower bound giving by the summed reading time in *seconds* more closely relates to the markdown word count.\n",
"\n",
"However, the larger estimate perhaps does also factor in context switching time going from one cell to another. Whilst this may be invisible to the reader if a markdown cell follows a markdown cell, it may be more evident when going from a markdown cell to a code cell. On the other hand, if a markdown cell follows another because there is a change from one subsection to another, there may be a pause for reflection as part of that context switch that *is* captured by the rounding."
]
},
{
"cell_type": "code",
"execution_count": 96,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th></th>\n",
" <th>n_total_code_lines</th>\n",
" <th>n_words</th>\n",
" <th>reading_time_mins</th>\n",
" <th>reading_time_s</th>\n",
" </tr>\n",
" <tr>\n",
" <th>path</th>\n",
" <th>filename</th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" <th></th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th rowspan=\"6\" valign=\"top\">../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks</th>\n",
" <th>../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks/02.1 Pandas Dataframes.ipynb</th>\n",
" <td>0</td>\n",
" <td>1763</td>\n",
" <td>61</td>\n",
" <td>1077.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks/02.2 Data file formats.ipynb</th>\n",
" <td>0</td>\n",
" <td>171</td>\n",
" <td>5</td>\n",
" <td>107.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks/02.2.0 Data file formats - file encodings.ipynb</th>\n",
" <td>0</td>\n",
" <td>706</td>\n",
" <td>24</td>\n",
" <td>430.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks/02.2.1 Data file formats - CSV.ipynb</th>\n",
" <td>0</td>\n",
" <td>1665</td>\n",
" <td>41</td>\n",
" <td>987.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks/02.2.2 Data file formats - JSON.ipynb</th>\n",
" <td>0</td>\n",
" <td>443</td>\n",
" <td>17</td>\n",
" <td>270.0</td>\n",
" </tr>\n",
" <tr>\n",
" <th>../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks/02.2.3 Data file formats - other.ipynb</th>\n",
" <td>0</td>\n",
" <td>825</td>\n",
" <td>21</td>\n",
" <td>499.0</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" n_total_code_lines \\\n",
"path filename \n",
"../Documents/GitHub/tm351-undercertainty/notebo... ../Documents/GitHub/tm351-undercertainty/notebo... 0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 0 \n",
"\n",
" n_words \\\n",
"path filename \n",
"../Documents/GitHub/tm351-undercertainty/notebo... ../Documents/GitHub/tm351-undercertainty/notebo... 1763 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 171 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 706 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 1665 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 443 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 825 \n",
"\n",
" reading_time_mins \\\n",
"path filename \n",
"../Documents/GitHub/tm351-undercertainty/notebo... ../Documents/GitHub/tm351-undercertainty/notebo... 61 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 5 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 24 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 41 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 17 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 21 \n",
"\n",
" reading_time_s \n",
"path filename \n",
"../Documents/GitHub/tm351-undercertainty/notebo... ../Documents/GitHub/tm351-undercertainty/notebo... 1077.0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 107.0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 430.0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 987.0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 270.0 \n",
" ../Documents/GitHub/tm351-undercertainty/notebo... 499.0 "
]
},
"execution_count": 96,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ddf.groupby(['path','filename'])[['n_total_code_lines','n_words',\n",
" 'reading_time_mins', 'reading_time_s' ]].sum()"
]
},
{
"cell_type": "code",
"execution_count": 101,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks': {'n_words': 5573,\n",
" 'reading_time_mins': 169,\n",
" 'reading_time_s': 3370.0}}"
]
},
"execution_count": 101,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ddf_dict = ddf.groupby(['path'])[['n_words', 'reading_time_mins', 'reading_time_s' ]].sum().to_dict(orient='index')\n",
"ddf_dict"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Reporting Templates\n",
"\n",
"It's all very well having the data in a dataframe, but it could be more useful to be able to generate some written reports. So what might an example report look like?\n",
"\n",
"How about something like:\n",
"\n",
"> In directory X there were N notebooks. The total markdown wordcount for notebooks in the directory was NN. The total number of lines of code across the notebooks was NN. The total estimated reading time across the notebooks was NN.\n",
">\n",
"> At the notebook level:\n",
"> - notebook A: markdown wordcount NN, lines of code NN, estimated reading time NN;\n",
"\n",
"It might also be useful to provide simple rule (cf. linter rules) that raise warnings about notebooks that go against best practice. For example, notebooks with word counts / code line counts or reading or completion times that exceed recommended limits."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's start with a simple template:"
]
},
{
"cell_type": "code",
"execution_count": 156,
"metadata": {},
"outputs": [],
"source": [
"report_template_simple_md = '''\n",
"In directory `{path}` there were {nb_count} notebooks.\n",
"The total markdown wordcount for the notebooks in the directory was {n_words} words,\n",
"with an estimated total reading time of {reading_time_mins} minutes.\n",
"'''"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can feed this from a `dict` containing fields required by the report template:"
]
},
{
"cell_type": "code",
"execution_count": 159,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks': {'n_words': 5573,\n",
" 'reading_time_mins': 169,\n",
" 'reading_time_s': 3370.0,\n",
" 'nb_count': 6,\n",
" 'path': '../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks'}}"
]
},
"execution_count": 159,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#%pip install deepmerge\n",
"from deepmerge import always_merger\n",
"\n",
"report_dict = always_merger.merge(ddf_dict, notebook_counts_by_dir )\n",
"for k in report_dict:\n",
" report_dict[k]['path'] = k\n",
"report_dict"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Feeding the `dict` to the template generates the report:"
]
},
{
"cell_type": "code",
"execution_count": 155,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'\\nIn directory `../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks` there were 6 notebooks.\\nThe total markdown wordcount for the notebooks in the directory was 5573 words,\\nwith an estimated total reading time 169 minutes.\\n'"
]
},
"execution_count": 155,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"report_template_simple_md.format(**report_dict[TEST_DIR])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create a function to make it easier to generate the feedstocl `dict`:"
]
},
{
"cell_type": "code",
"execution_count": 190,
"metadata": {},
"outputs": [],
"source": [
"def notebook_report_feedstock_md_test(ddf):\n",
" \"\"\"Create a feedstock dict for report generation. Keyed by directory path.\"\"\"\n",
" ddf_dict = ddf.groupby(['path'])[['n_words', 'reading_time_mins', 'reading_time_s' ]].sum().to_dict(orient='index')\n",
" \n",
" notebook_counts_by_dir = ddf.groupby(['path'])['filename'].nunique().to_dict()\n",
" notebook_counts_by_dir = {k:{'nb_count':notebook_counts_by_dir[k]} for k in notebook_counts_by_dir}\n",
" \n",
" report_dict = always_merger.merge(ddf_dict, notebook_counts_by_dir )\n",
" \n",
" for k in report_dict:\n",
" report_dict[k]['path'] = k\n",
" \n",
" return report_dict"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now use the `notebook_report_feedstock()` function to generate the feedstock `dict` directlry from the report dataframe:"
]
},
{
"cell_type": "code",
"execution_count": 162,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks': {'n_words': 5573,\n",
" 'reading_time_mins': 169,\n",
" 'reading_time_s': 3370.0,\n",
" 'nb_count': 6}}"
]
},
"execution_count": 162,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"notebook_report_feedstock_md_test(ddf)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Additional Reporting Levels\n",
"For additional reports, we could start to look for particular grammatical constructions in the markdown text."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"When it comes to looking for particular grammatical constructions in the text, the `textacy` package allows us to define patterns of interest in various ways. Are there any particular constructions that we may want to look out for in an instructional text?"
]
},
{
"cell_type": "code",
"execution_count": 44,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[provides, includes, intended, test, Note]"
]
},
"execution_count": 44,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"import textacy\n",
"\n",
"#But how do you define the pattern to extract the largest phrase over a sequence of tokens?\n",
"verb_phrase = r'(<VERB>?<ADV>*<VERB>+)' #extract.pos_regex_matches DEPRECATED\n",
"\n",
"verb_phrase2 = [{\"POS\": \"VERB\", \"OP\":\"?\"}, {\"POS\": \"ADV\", \"OP\": \"*\"},\n",
" {\"POS\": \"VERB\", \"OP\":\"+\"}] #extract.matches\n",
"\n",
"verb_phrase3 = r'POS:BERB:? POS:ADV:* POS:VERB:+' #extract.matches\n",
"\n",
"[vp for vp in textacy.extract.matches(doc, verb_phrase3)][:5]"
]
},
{
"cell_type": "code",
"execution_count": 45,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'SYM': 1,\n",
" 'PROPN': 4,\n",
" 'ADP': 5,\n",
" 'SPACE': 3,\n",
" 'DET': 6,\n",
" 'NOUN': 12,\n",
" 'VERB': 7,\n",
" 'PUNCT': 3,\n",
" 'PRON': 1,\n",
" 'CCONJ': 1,\n",
" 'PART': 1,\n",
" 'ADJ': 1,\n",
" 'ADV': 2}"
]
},
"execution_count": 45,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from collections import Counter\n",
"dict(Counter(([token.pos_ for token in doc])))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Code Cell Analysis\n",
"\n",
"As well as reporting on markdown cells, we can also generate reports on code cells. (We could also use similar techiques to report on code blocks found in markdown cells.)\n",
"\n",
"Possible code cell reports include reporting on:\n",
"\n",
"- packages imported into a notebook;\n",
"- number of lines of code / code comments;\n",
"- code complexity.\n",
"\n",
"We could also run static analysis tests over *all* the code loaded into a notebook, for example using things like [`importchecker`](https://github.com/zopefoundation/importchecker) to check that imports are actually used.\n",
"\n",
"Checks against whether code cells in a notebooks: a) have been run; b) whether they have been run in order are also possible. If we extend the analysis to code cell outputs, we could also report on whether cells had been run without warning or error and what sort of output they produced.\n",
"\n",
"Tools such as [`pyflakes`](https://github.com/PyCQA/pyflakes) can also be used to run a wider range of static tests over a codebase, as can other code linters. See also [*Thinking About Things That Might Be Autogradeable or Useful for Automated Marking Support*](https://blog.ouseful.info/2019/12/10/thinking-about-things-that-might-be-autogradeable/) for examples of tests that may be used in autograding, some of which might also be useful for notebook code profiling.\n",
"\n",
"It might also be worth trying to collate possible useful guidelines / heuristics / rules of thumb for creating notebooks that could also provide the basis of quality minded linting checks.\n",
"\n",
"For example:\n",
"\n",
"- a markdown cell should always appear before a code cells to set the context for what the code cell is expected to achieve;\n",
"- a markdown cell commenting on the output of a code cell immediately preceding may be appropriate in some cases;\n",
"- one cell should be defined per code cell. A markdown cell immediately following a code cell that defines a function might include a line of text that might also serve as the function doc text, describing what the function does an dprefacing a code cell that demonstrates the behaviour of the function."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Generating code reports over a single notebook\n",
"\n",
"Let's start to put together some metrics we can run against code cells, either at an individual level or from code aggregated from across all the code cells in a notebook."
]
},
{
"cell_type": "code",
"execution_count": 46,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['abjad', 'numpy', 'pandas', 'IPython.dsiplay']"
]
},
"execution_count": 46,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"c='''#print\\nimport pandas\\n\\nprint('a')\\nimport abjad\\nimport numpy as np\\nfrom IPython.dsiplay import HTML, JSON'''\n",
"\n",
"#https://github.com/andrewp-as-is/list-imports.py #list imports\n",
"#%pip install list-imports\n",
"import list_imports\n",
"list_imports.parse(c)\n",
"#Would also need to capture magics?\n",
"\n",
"# TO DO - NOT CURRENTLY REPORTED"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Some utilities may not make sense in the reporting when applied at a cell level. For example, it's quite likely that a package imported into a cell may not be used in that cell, which `pyflakes` would report unfavourably on:"
]
},
{
"cell_type": "code",
"execution_count": 47,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"\"dummy:1: 'pandas as pd' imported but unused\\n\""
]
},
"execution_count": 47,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#%pip install pyflakes\n",
"#pyflakes seems to print the report, so we'd need to find a way to capture it\n",
"from pyflakes.api import check\n",
"from pyflakes.reporter import Reporter\n",
"\n",
"import io\n",
"\n",
"output_w = io.StringIO()\n",
"output_e = io.StringIO()\n",
"\n",
"check('''import pandas as pd''', 'dummy', Reporter(output_w, output_e))\n",
"output_w.getvalue()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Another form of analysis that only makes sense at the notebook level is the code cell execution analysis:"
]
},
{
"cell_type": "code",
"execution_count": 48,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[None, None, 1, None, None] False True\n"
]
}
],
"source": [
"# Check execution across notebook - TO DO - NOT CURRENTLY REPORTED\n",
"cell_execution_order = []\n",
"num_code_cells = 0\n",
"for cell in nb.cells:\n",
" if cell['cell_type']=='code':\n",
" cell_execution_order.append(cell['execution_count'])\n",
" num_code_cells = num_code_cells + 1\n",
"\n",
"\n",
"_executed_cells = [i for i in cell_execution_order if i is not None and isinstance(i,int) ]\n",
"in_order_execution = _executed_cells == sorted(_executed_cells)\n",
"\n",
"all_cells_executed = len(_executed_cells)==num_code_cells\n",
"print(cell_execution_order, all_cells_executed, in_order_execution,)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Parsing IPython Code\n",
"\n",
"One thing to bear in mind is that code cells may contain block magic that switches code from the assumed default Python code to potentially a different language. For this reason, we might want to fall back from the `radon` metrics as a result of trying to load code into a Python AST parser when meeting cells that employ cell block magic, or explore whether an IPyhton parser could be used instead."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's try to cleanse IPython directives such as shell commands (`!` prefix) or magics (`%` prefix) from a code string so that we can present it to `radon`."
]
},
{
"cell_type": "code",
"execution_count": 49,
"metadata": {},
"outputs": [],
"source": [
"def sanitise_IPython_code(c):\n",
" \"\"\"Cleanse an IPython code string so we can parse it with radon.\"\"\"\n",
" #Comment out magic and shell commands\n",
" c = '\\n'.join([f'#{_r}' if _r.lstrip().startswith(('%','!')) else _r for _r in c.splitlines()])\n",
" \n",
" return c"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `sanitise_IPython_code()` function partially sanitises an IPython code string so that it can be passed to, and parsed by, the `radon`. Note that where magic or shell statements are used on the right hand side of an assignment statement, this will still cause an error. "
]
},
{
"cell_type": "code",
"execution_count": 50,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"#%load_ext magic\n",
"import pandas\n",
"\n",
"#!ls\n",
"print(a)\n"
]
},
{
"data": {
"text/plain": [
"(5, 1, 2, 2)"
]
},
"execution_count": 50,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#Use the `radon` analyzer\n",
"#%pip install radon\n",
"from radon.raw import analyze\n",
"\n",
"c = '''%load_ext magic\\nimport pandas\\n\\n!ls\\nprint(a)'''\n",
"c = sanitise_IPython_code(c)\n",
"\n",
"print(c)\n",
"n_total_code_lines, n_blank_code_lines, \\\n",
" n_single_line_comment_code_lines, n_code_lines = r_analyze(sanitise_IPython_code(c))\n",
"\n",
"n_total_code_lines, n_blank_code_lines, n_single_line_comment_code_lines, n_code_lines"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To parse a code cell, we can try to use the `radon` analyser, with a sanitised code string, or fall back to using the simpler code sanitiser. It will also be convenient to return the results as a Python `dict` object."
]
},
{
"cell_type": "code",
"execution_count": 51,
"metadata": {},
"outputs": [],
"source": [
"def robust_code_cell_analyse(c, parser='radon'):\n",
" \"\"\"Use the `radon` code analyser if we can else fall back to the simple custom code analyser.\"\"\"\n",
" \n",
" def cleansed_radon(c):\n",
" return r_analyze(sanitise_IPython_code(c))\n",
" \n",
" if c.startswith('%%'):\n",
" #use local code analyser\n",
" parser = 'local'\n",
"\n",
" if parser == 'radon':\n",
" try:\n",
" _response = cleansed_radon(c)\n",
" except:\n",
" #fallback to simple analyser\n",
" _response = code_block_report(c)\n",
" else:\n",
" _response = code_block_report(c)\n",
" \n",
" (n_total_code_lines, n_blank_code_lines, \\\n",
" n_single_line_comment_code_lines, n_code_lines) = _response\n",
" \n",
" _reading_time = code_reading_time(n_code_lines, n_single_line_comment_code_lines)\n",
" \n",
" response = {\n",
" 'n_total_code_lines': n_total_code_lines,\n",
" 'n_blank_code_lines': n_blank_code_lines,\n",
" 'n_single_line_comment_code_lines': n_single_line_comment_code_lines,\n",
" 'n_code_lines': n_code_lines,\n",
" 'n_screen_lines':n_total_code_lines,\n",
" 'reading_time_s':_reading_time,\n",
" 'reading_time_mins': math.ceil(_reading_time/60)\n",
" }\n",
" \n",
" return response"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The robust analyser should cope with a variety of strings."
]
},
{
"cell_type": "code",
"execution_count": 52,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"{'n_total_code_lines': 4, 'n_blank_code_lines': 1, 'n_single_line_comment_code_lines': 2, 'n_code_lines': 1, 'n_screen_lines': 4, 'reading_time_s': 3, 'reading_time_mins': 1}\n",
"{'n_total_code_lines': 2, 'n_blank_code_lines': 0, 'n_single_line_comment_code_lines': 0, 'n_code_lines': 2, 'n_screen_lines': 2, 'reading_time_s': 2, 'reading_time_mins': 1}\n"
]
}
],
"source": [
"print(robust_code_cell_analyse('import pandas\\n\\n# comment\\n!ls'))\n",
"print(robust_code_cell_analyse('%%sql\\nSELECT * FROM TABLE'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We now need to start pulling together a function that we can cal to run the basic report and other code cell reports."
]
},
{
"cell_type": "code",
"execution_count": 53,
"metadata": {},
"outputs": [],
"source": [
"def process_notebook_code_text(txt):\n",
" \"\"\"Generate code cell report.\"\"\"\n",
" report = pd.DataFrame()\n",
" basic_code_report = robust_code_cell_analyse(txt)\n",
" return pd.DataFrame([{'text':txt,\n",
" **basic_code_report }])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The report generates a single row report dataframe from a code string:"
]
},
{
"cell_type": "code",
"execution_count": 54,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>text</th>\n",
" <th>n_total_code_lines</th>\n",
" <th>n_blank_code_lines</th>\n",
" <th>n_single_line_comment_code_lines</th>\n",
" <th>n_code_lines</th>\n",
" <th>n_screen_lines</th>\n",
" <th>reading_time_s</th>\n",
" <th>reading_time_mins</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>import pandas\\n\\n# comment\\n!ls</td>\n",
" <td>4</td>\n",
" <td>1</td>\n",
" <td>2</td>\n",
" <td>1</td>\n",
" <td>4</td>\n",
" <td>3</td>\n",
" <td>1</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>"
],
"text/plain": [
" text n_total_code_lines n_blank_code_lines \\\n",
"0 import pandas\\n\\n# comment\\n!ls 4 1 \n",
"\n",
" n_single_line_comment_code_lines n_code_lines n_screen_lines \\\n",
"0 2 1 4 \n",
"\n",
" reading_time_s reading_time_mins \n",
"0 3 1 "
]
},
"execution_count": 54,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"process_notebook_code_text('import pandas\\n\\n# comment\\n!ls')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In order to process code cells as well as markdown cells in our notebook processer, we will need build on the `process_notebook_md()` function to create a more general one. Note that the current approach will give us an inefficient dataframe, column wise, in that whilst each row represents the report from a code cell *or* a markdown cell, the columns cover reports from both code *and* markdown cells."
]
},
{
"cell_type": "code",
"execution_count": 223,
"metadata": {},
"outputs": [],
"source": [
"def process_notebook(nb, fn=''):\n",
" \"\"\"Process all the markdown and code cells in a notebook.\"\"\"\n",
" cell_reports = pd.DataFrame()\n",
" \n",
" for i, cell in enumerate(nb.cells):\n",
" if cell['cell_type']=='markdown':\n",
" _metrics = process_notebook_md_doc( nlp( cell['source'] ))\n",
" _metrics['cell_count'] = i\n",
" _metrics['cell_type'] = 'md'\n",
" cell_reports = cell_reports.append(_metrics, sort=False)\n",
" elif cell['cell_type']=='code':\n",
" _metrics = process_notebook_code_text(cell['source'] )\n",
" _metrics['cell_count'] = i\n",
" _metrics['cell_type'] = 'code'\n",
" cell_reports = cell_reports.append(_metrics, sort=False)\n",
" \n",
" cell_reports['filename'] = fn\n",
" cell_reports.reset_index(drop=True, inplace=True)\n",
" return cell_reports"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We should now be able to generate a report that includes statistics from code as well as markdown cells."
]
},
{
"cell_type": "code",
"execution_count": 224,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>text</th>\n",
" <th>n_sents</th>\n",
" <th>n_words</th>\n",
" <th>n_chars</th>\n",
" <th>n_syllables</th>\n",
" <th>n_unique_words</th>\n",
" <th>n_long_words</th>\n",
" <th>n_monosyllable_words</th>\n",
" <th>n_polysyllable_words</th>\n",
" <th>flesch_kincaid_grade_level</th>\n",
" <th>...</th>\n",
" <th>reading_time_s</th>\n",
" <th>reading_time_mins</th>\n",
" <th>mean_sentence_length</th>\n",
" <th>median_sentence_length</th>\n",
" <th>stdev_sentence_length</th>\n",
" <th>keyterms</th>\n",
" <th>acronyms</th>\n",
" <th>cell_count</th>\n",
" <th>cell_type</th>\n",
" <th>filename</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td># Test Notebook for Notebook Profiler\\n\\nThis ...</td>\n",
" <td>4.0</td>\n",
" <td>40.0</td>\n",
" <td>203.0</td>\n",
" <td>63.0</td>\n",
" <td>27.0</td>\n",
" <td>15.0</td>\n",
" <td>25.0</td>\n",
" <td>6.0</td>\n",
" <td>6.895000</td>\n",
" <td>...</td>\n",
" <td>25.0</td>\n",
" <td>1</td>\n",
" <td>11.00</td>\n",
" <td>9.5</td>\n",
" <td>4.966555</td>\n",
" <td>[(notebook profiler, 0.08196495093971548), (te...</td>\n",
" <td>{}</td>\n",
" <td>0</td>\n",
" <td>md</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>## Markdown Cells With Code Blocks\\n\\nThis cel...</td>\n",
" <td>4.0</td>\n",
" <td>30.0</td>\n",
" <td>123.0</td>\n",
" <td>38.0</td>\n",
" <td>22.0</td>\n",
" <td>5.0</td>\n",
" <td>23.0</td>\n",
" <td>1.0</td>\n",
" <td>2.281667</td>\n",
" <td>...</td>\n",
" <td>18.0</td>\n",
" <td>1</td>\n",
" <td>8.25</td>\n",
" <td>5.5</td>\n",
" <td>8.261356</td>\n",
" <td>[(single code block, 0.09825762538579677), (Ma...</td>\n",
" <td>{}</td>\n",
" <td>1</td>\n",
" <td>md</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>This cell contains two code blocks.\\n\\nHere's ...</td>\n",
" <td>8.0</td>\n",
" <td>49.0</td>\n",
" <td>173.0</td>\n",
" <td>58.0</td>\n",
" <td>23.0</td>\n",
" <td>6.0</td>\n",
" <td>41.0</td>\n",
" <td>1.0</td>\n",
" <td>0.766097</td>\n",
" <td>...</td>\n",
" <td>28.0</td>\n",
" <td>1</td>\n",
" <td>6.50</td>\n",
" <td>6.0</td>\n",
" <td>4.105745</td>\n",
" <td>[(code block, 0.052250174985765105), (import p...</td>\n",
" <td>{}</td>\n",
" <td>2</td>\n",
" <td>md</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td># This is a code cell\\nimport pandas\\n\\n#Creat...</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>...</td>\n",
" <td>4.0</td>\n",
" <td>1</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>3</td>\n",
" <td>code</td>\n",
" <td></td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td># This is a code cell with a magic...\\n\\n%matp...</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>...</td>\n",
" <td>5.0</td>\n",
" <td>1</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>NaN</td>\n",
" <td>4</td>\n",
" <td>code</td>\n",
" <td></td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"<p>5 rows × 40 columns</p>\n",
"</div>"
],
"text/plain": [
" text n_sents n_words \\\n",
"0 # Test Notebook for Notebook Profiler\\n\\nThis ... 4.0 40.0 \n",
"1 ## Markdown Cells With Code Blocks\\n\\nThis cel... 4.0 30.0 \n",
"2 This cell contains two code blocks.\\n\\nHere's ... 8.0 49.0 \n",
"3 # This is a code cell\\nimport pandas\\n\\n#Creat... NaN NaN \n",
"4 # This is a code cell with a magic...\\n\\n%matp... NaN NaN \n",
"\n",
" n_chars n_syllables n_unique_words n_long_words n_monosyllable_words \\\n",
"0 203.0 63.0 27.0 15.0 25.0 \n",
"1 123.0 38.0 22.0 5.0 23.0 \n",
"2 173.0 58.0 23.0 6.0 41.0 \n",
"3 NaN NaN NaN NaN NaN \n",
"4 NaN NaN NaN NaN NaN \n",
"\n",
" n_polysyllable_words flesch_kincaid_grade_level ... reading_time_s \\\n",
"0 6.0 6.895000 ... 25.0 \n",
"1 1.0 2.281667 ... 18.0 \n",
"2 1.0 0.766097 ... 28.0 \n",
"3 NaN NaN ... 4.0 \n",
"4 NaN NaN ... 5.0 \n",
"\n",
" reading_time_mins mean_sentence_length median_sentence_length \\\n",
"0 1 11.00 9.5 \n",
"1 1 8.25 5.5 \n",
"2 1 6.50 6.0 \n",
"3 1 NaN NaN \n",
"4 1 NaN NaN \n",
"\n",
" stdev_sentence_length keyterms \\\n",
"0 4.966555 [(notebook profiler, 0.08196495093971548), (te... \n",
"1 8.261356 [(single code block, 0.09825762538579677), (Ma... \n",
"2 4.105745 [(code block, 0.052250174985765105), (import p... \n",
"3 NaN NaN \n",
"4 NaN NaN \n",
"\n",
" acronyms cell_count cell_type filename \n",
"0 {} 0 md \n",
"1 {} 1 md \n",
"2 {} 2 md \n",
"3 NaN 3 code \n",
"4 NaN 4 code \n",
"\n",
"[5 rows x 40 columns]"
]
},
"execution_count": 224,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"report = process_notebook(nb)\n",
"report.head(5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's just check what columns we are potentially reporting on:"
]
},
{
"cell_type": "code",
"execution_count": 225,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Index(['text', 'n_sents', 'n_words', 'n_chars', 'n_syllables',\n",
" 'n_unique_words', 'n_long_words', 'n_monosyllable_words',\n",
" 'n_polysyllable_words', 'flesch_kincaid_grade_level',\n",
" 'flesch_reading_ease', 'smog_index', 'gunning_fog_index',\n",
" 'coleman_liau_index', 'automated_readability_index', 'lix',\n",
" 'gulpease_index', 'wiener_sachtextformel', 'n_headers', 'n_paras',\n",
" 'n_screen_lines', 's_lengths', 's_mean', 's_median', 's_stdev',\n",
" 'n_code_blocks', 'n_total_code_lines', 'n_code_lines',\n",
" 'n_blank_code_lines', 'n_single_line_comment_code_lines',\n",
" 'reading_time_s', 'reading_time_mins', 'mean_sentence_length',\n",
" 'median_sentence_length', 'stdev_sentence_length', 'keyterms',\n",
" 'acronyms', 'cell_count', 'cell_type', 'filename'],\n",
" dtype='object')"
]
},
"execution_count": 225,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"report.columns"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And let's see if our directory processor now also includes code cell statistics:"
]
},
{
"cell_type": "code",
"execution_count": 226,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"md 160\n",
"code 119\n",
"Name: cell_type, dtype: int64"
]
},
"execution_count": 226,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ddf2 = nb_multidir_profiler('../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks')\n",
"ddf2['cell_type'].value_counts()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's also check to see how the code cells are reported:"
]
},
{
"cell_type": "code",
"execution_count": 229,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"n_code_blocks 0.0\n",
"n_total_code_lines 390.0\n",
"n_code_lines 228.0\n",
"n_blank_code_lines 25.0\n",
"n_single_line_comment_code_lines 137.0\n",
"dtype: float64"
]
},
"execution_count": 229,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"code_cols = [c for c in ddf2.columns if 'code' in c]\n",
"ddf2[ddf2['cell_type']=='code'][code_cols].sum()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Generating Reports Across Multiple Directories\n",
"\n",
"We are now in a position to start generating rich report for notebooks across several directories.\n",
"\n",
"Let's grab data for notebooks across an example set of directories:"
]
},
{
"cell_type": "code",
"execution_count": 231,
"metadata": {},
"outputs": [],
"source": [
"ddf3 = nb_multidir_profiler('../Documents/GitHub/tm351-undercertainty/notebooks/tm351/')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And just quickly test we can generate a report that summarises the notebooks in each directory:"
]
},
{
"cell_type": "code",
"execution_count": 232,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"\n",
"In directory `../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 01 Notebooks` there were 5 notebooks.\n",
"The total markdown wordcount for the notebooks in the directory was 3033.0 words,\n",
"with an estimated total reading time of 143 minutes.\n",
"\n",
"\n",
"\n",
"In directory `../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks` there were 6 notebooks.\n",
"The total markdown wordcount for the notebooks in the directory was 5573.0 words,\n",
"with an estimated total reading time of 288 minut\n"
]
}
],
"source": [
"big_feedstock = notebook_report_feedstock_md_test(ddf3)\n",
"report_txt=''\n",
"for d in big_feedstock:\n",
" if 'tm351/Part ' in d:\n",
" report_txt = report_txt + '\\n\\n' + report_template_simple_md.format(**big_feedstock[d])\n",
" \n",
"print(report_txt[:500])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's update the report template and the report feedstock function.\n",
"\n",
"First, what shall we report on?"
]
},
{
"cell_type": "code",
"execution_count": 210,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Index(['filename', 'text', 'n_sents', 'n_words', 'n_chars', 'n_syllables',\n",
" 'n_unique_words', 'n_long_words', 'n_monosyllable_words',\n",
" 'n_polysyllable_words', 'flesch_kincaid_grade_level',\n",
" 'flesch_reading_ease', 'smog_index', 'gunning_fog_index',\n",
" 'coleman_liau_index', 'automated_readability_index', 'lix',\n",
" 'gulpease_index', 'wiener_sachtextformel', 'n_headers', 'n_paras',\n",
" 'n_screen_lines', 's_lengths', 's_mean', 's_median', 's_stdev',\n",
" 'n_code_blocks', 'n_total_code_lines', 'n_code_lines',\n",
" 'n_blank_code_lines', 'n_single_line_comment_code_lines',\n",
" 'reading_time_s', 'reading_time_mins', 'mean_sentence_length',\n",
" 'median_sentence_length', 'stdev_sentence_length', 'keyterms',\n",
" 'acronyms', 'cell_count', 'cell_type', 'path'],\n",
" dtype='object')"
]
},
"execution_count": 210,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"ddf3.columns"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's make a start on a complete report template..."
]
},
{
"cell_type": "code",
"execution_count": 304,
"metadata": {},
"outputs": [],
"source": [
"report_template_full = '''\n",
"In directory `{path}` there were {nb_count} notebooks.\n",
"\n",
"- total markdown wordcount {n_words} words across {n_md_cells} markdown cells\n",
"- total code line count of {n_total_code_lines} lines of code across {n_code_cells} code cells\n",
" - {n_code_lines} code lines, {n_single_line_comment_code_lines} comment lines and {n_blank_code_lines} blank lines\n",
"\n",
"Estimated total reading time of {reading_time_mins} minutes.\n",
"\n",
"'''"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now let's add those extra requirements to the the feedstock generator:"
]
},
{
"cell_type": "code",
"execution_count": 300,
"metadata": {},
"outputs": [],
"source": [
"def notebook_report_feedstock(ddf):\n",
" \"\"\"Create a feedstock dict for report generation. Keyed by directory path.\"\"\"\n",
" ddf_dict = ddf.groupby(['path'])[['n_words', 'reading_time_mins', 'reading_time_s',\n",
" 'n_code_lines', 'n_single_line_comment_code_lines',\n",
" 'n_total_code_lines','n_blank_code_lines']].sum().to_dict(orient='index')\n",
" \n",
" notebook_counts_by_dir = ddf.groupby(['path'])['filename'].nunique().to_dict()\n",
" notebook_counts_by_dir = {k:{'nb_count':notebook_counts_by_dir[k]} for k in notebook_counts_by_dir}\n",
" \n",
" report_dict = always_merger.merge(ddf_dict, notebook_counts_by_dir )\n",
" \n",
" code_cell_counts = ddf[ddf['cell_type']=='code'].groupby(['path']).size().to_dict()\n",
" md_cell_counts = ddf[ddf['cell_type']=='md'].groupby(['path']).size().to_dict()\n",
" \n",
" for k in report_dict:\n",
" report_dict[k]['path'] = k\n",
" report_dict[k]['n_code_cells'] = code_cell_counts[k] if k in code_cell_counts else 'NA'\n",
" report_dict[k]['n_md_cells'] = md_cell_counts[k] if k in md_cell_counts else 'NA'\n",
" \n",
" return report_dict"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Create a wrapper function for generating the report text:"
]
},
{
"cell_type": "code",
"execution_count": 301,
"metadata": {},
"outputs": [],
"source": [
"def reporter(df, template, path_filter=''):\n",
" feedstock = notebook_report_feedstock(df)\n",
" report_txt=''\n",
" for d in feedstock:\n",
" if path_filter in d:\n",
" report_txt = report_txt + '\\n\\n' + template.format(**feedstock[d])\n",
" return report_txt"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now use the `reporter()` function to generate a report based on filtered paths from a report dataframe and a template:"
]
},
{
"cell_type": "code",
"execution_count": 302,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"\n",
"In directory `../Documents/GitHub/tm351-undercertainty/notebooks/tm351/Part 02 Notebooks` there were 6 notebooks.\n",
"\n",
"- total markdown wordcount 5573.0 words across 160\n",
"- total code line count of 390 lines of code across 119 code cells\n",
" - 228 code lines, 137 comment lines and 25 blank lines\n",
"\n",
"Estimated total reading time of 288 minutes.\n",
"\n",
"\n"
]
}
],
"source": [
"print(reporter(ddf2, report_template_full, 'tm351/Part '))"
]
},
{
"cell_type": "code",
"execution_count": 305,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"\n",
"\n",
"In directory `Part 01 Notebooks` there were 5 notebooks.\n",
"\n",
"- total markdown wordcount 3033.0 words across 65 markdown cells\n",
"- total code line count of 571 lines of code across 65 code cells\n",
" - 327 code lines, 160 comment lines and 84 blank lines\n",
"\n",
"Estimated total reading time of 143 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 02 Notebooks` there were 6 notebooks.\n",
"\n",
"- total markdown wordcount 5573.0 words across 160 markdown cells\n",
"- total code line count of 390 lines of code across 119 code cells\n",
" - 228 code lines, 137 comment lines and 25 blank lines\n",
"\n",
"Estimated total reading time of 288 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 03 Notebooks` there were 4 notebooks.\n",
"\n",
"- total markdown wordcount 11027.0 words across 230 markdown cells\n",
"- total code line count of 808 lines of code across 181 code cells\n",
" - 606 code lines, 131 comment lines and 72 blank lines\n",
"\n",
"Estimated total reading time of 444 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 04 Notebooks` there were 8 notebooks.\n",
"\n",
"- total markdown wordcount 11992.0 words across 232 markdown cells\n",
"- total code line count of 917 lines of code across 259 code cells\n",
" - 595 code lines, 260 comment lines and 64 blank lines\n",
"\n",
"Estimated total reading time of 518 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 05 Notebooks` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 8499.0 words across 105 markdown cells\n",
"- total code line count of 978 lines of code across 84 code cells\n",
" - 510 code lines, 322 comment lines and 147 blank lines\n",
"\n",
"Estimated total reading time of 231 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 07 Notebooks` there were 2 notebooks.\n",
"\n",
"- total markdown wordcount 6024.0 words across 106 markdown cells\n",
"- total code line count of 0 lines of code across NA code cells\n",
" - 0 code lines, 0 comment lines and 0 blank lines\n",
"\n",
"Estimated total reading time of 127 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 08 Notebooks` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 12612.0 words across 383 markdown cells\n",
"- total code line count of 770 lines of code across 155 code cells\n",
" - 563 code lines, 59 comment lines and 163 blank lines\n",
"\n",
"Estimated total reading time of 552 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 09 Notebooks` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 9856.0 words across 254 markdown cells\n",
"- total code line count of 502 lines of code across 110 code cells\n",
" - 359 code lines, 48 comment lines and 105 blank lines\n",
"\n",
"Estimated total reading time of 384 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 10 Notebooks` there were 5 notebooks.\n",
"\n",
"- total markdown wordcount 11511.0 words across 303 markdown cells\n",
"- total code line count of 802 lines of code across 170 code cells\n",
" - 616 code lines, 66 comment lines and 145 blank lines\n",
"\n",
"Estimated total reading time of 506 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 11 Notebooks` there were 6 notebooks.\n",
"\n",
"- total markdown wordcount 17442.0 words across 437 markdown cells\n",
"- total code line count of 1586 lines of code across 250 code cells\n",
" - 1357 code lines, 86 comment lines and 154 blank lines\n",
"\n",
"Estimated total reading time of 733 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 12 Notebooks` there were 2 notebooks.\n",
"\n",
"- total markdown wordcount 6570.0 words across 242 markdown cells\n",
"- total code line count of 657 lines of code across 160 code cells\n",
" - 570 code lines, 30 comment lines and 53 blank lines\n",
"\n",
"Estimated total reading time of 413 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 12 Notebooks/optional_part_12` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 846.0 words across 21 markdown cells\n",
"- total code line count of 51 lines of code across 14 code cells\n",
" - 37 code lines, 5 comment lines and 9 blank lines\n",
"\n",
"Estimated total reading time of 39 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 14 Notebooks` there were 8 notebooks.\n",
"\n",
"- total markdown wordcount 7077.0 words across 148 markdown cells\n",
"- total code line count of 825 lines of code across 197 code cells\n",
" - 641 code lines, 105 comment lines and 78 blank lines\n",
"\n",
"Estimated total reading time of 359 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 15 Notebooks` there were 10 notebooks.\n",
"\n",
"- total markdown wordcount 4434.0 words across 121 markdown cells\n",
"- total code line count of 1314 lines of code across 208 code cells\n",
" - 1077 code lines, 108 comment lines and 138 blank lines\n",
"\n",
"Estimated total reading time of 336 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 16 Notebooks` there were 6 notebooks.\n",
"\n",
"- total markdown wordcount 2214.0 words across 62 markdown cells\n",
"- total code line count of 527 lines of code across 123 code cells\n",
" - 454 code lines, 51 comment lines and 22 blank lines\n",
"\n",
"Estimated total reading time of 189 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 20 Notebooks` there were 2 notebooks.\n",
"\n",
"- total markdown wordcount 2219.0 words across 59 markdown cells\n",
"- total code line count of 208 lines of code across 24 code cells\n",
" - 124 code lines, 24 comment lines and 46 blank lines\n",
"\n",
"Estimated total reading time of 84 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 21 Notebooks` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 2200.0 words across 64 markdown cells\n",
"- total code line count of 426 lines of code across 45 code cells\n",
" - 273 code lines, 44 comment lines and 109 blank lines\n",
"\n",
"Estimated total reading time of 110 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 22 Notebooks` there were 4 notebooks.\n",
"\n",
"- total markdown wordcount 5431.0 words across 174 markdown cells\n",
"- total code line count of 528 lines of code across 100 code cells\n",
" - 355 code lines, 58 comment lines and 109 blank lines\n",
"\n",
"Estimated total reading time of 279 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 23 Notebooks` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 7645.0 words across 187 markdown cells\n",
"- total code line count of 576 lines of code across 109 code cells\n",
" - 384 code lines, 79 comment lines and 138 blank lines\n",
"\n",
"Estimated total reading time of 312 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 25 Notebooks` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 7447.0 words across 119 markdown cells\n",
"- total code line count of 890 lines of code across 64 code cells\n",
" - 563 code lines, 181 comment lines and 144 blank lines\n",
"\n",
"Estimated total reading time of 220 minutes.\n",
"\n",
"\n",
"\n",
"\n",
"In directory `Part 26 Notebooks` there were 3 notebooks.\n",
"\n",
"- total markdown wordcount 3993.0 words across 82 markdown cells\n",
"- total code line count of 828 lines of code across 45 code cells\n",
" - 535 code lines, 130 comment lines and 153 blank lines\n",
"\n",
"Estimated total reading time of 141 minutes.\n",
"\n",
"\n"
]
}
],
"source": [
"print(reporter(ddf3, report_template_full, 'tm351/Part ').replace('../Documents/GitHub/tm351-undercertainty/notebooks/tm351/',''))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visualising Notebook Structure\n",
"\n",
"To provide a glanceable, macroscopic way of comparing the size and structure of multiple notebooks, we can generate a simple visualisation based on screen line counts and colour codes for different cell types or cell state.\n",
"\n",
"Reports that include cell index and a simple line count (for example, reprting the number of code lines or screen lines for markdown) can be rendered directly as linear visualisations showing the overall structure of a notebook. \n",
"\n",
"For example:\n",
"\n",
"- markdown: header;\n",
"- markdown: paragraph;\n",
"- markdown: code block;\n",
"- markdown: blank line;\n",
"- code: code;\n",
"- code: comment;\n",
"- code: magic;\n",
"- code: blank line;\n",
"- other: other cells.\n",
"\n",
"To profile within a cell requires access to cell internals, or generating a cell profile during cell processing.\n",
"\n",
"However, it's easy enough to generate a view over the code and markdown cells.\n",
"\n",
"Let's start by exploring a simple representation:"
]
},
{
"cell_type": "code",
"execution_count": 59,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAV0AAADnCAYAAAC9roUQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAADb0lEQVR4nO3coXUDMRQF0ShnK0gJoS7C1aQqNxMeHOpW5AJsogUj4Huh0ENzPtKYc34A0PjcPQDgnYguQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYDQsXsALBvj+Wu8OceGJbDMpQsQEl2AkOgChEQXICS6ACHRBQiJLkBIdAFCogsQEl2AkOgChEQXICS6ACHRBQiJLkBIdAFCogsQEl2AkOgChEQXICS6ACHRBQiJLkBIdAFCogsQEl2AkOgChEQXICS6ACHRBQiJLkBIdAFCx+4BsOz3b/cCOM2lCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxA6dg+AVT//309vt+uGIXCCSxcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKEjt0DYNXtcn/x+pXvgDNcugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgJDoAoREFyAkugAh0QUIiS5ASHQBQqILEBJdgNCYc+7eAPA2XLoAIdEFCIkuQEh0AUKiCxASXYCQ6AKERBcgJLoAIdEFCIkuQEh0AUKiCxB6ANR4EEa6ZpCIAAAAAElFTkSuQmCC\n",
"text/plain": [
"<Figure size 432x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"\n",
"fig, ax = plt.subplots()\n",
"ax.axis('off')\n",
"\n",
"#Simple representation of lines per cell and cell colour based on cell type\n",
"n_c = [(1,'r'),(2,'pink'), (1,'cornflowerblue'), (2,'pink')]\n",
"\n",
"x=0\n",
"y=0\n",
"\n",
"for _n_c in n_c:\n",
" _y = y + _n_c[0]\n",
" plt.plot([x,x], [y,_y], _n_c[1], linewidth=5)\n",
" y = _y #may want to add a gap when moving from one cell to next\n",
"plt.gca().invert_yaxis()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can get the list of cell size and colour tuples from a notebook's report data frame:"
]
},
{
"cell_type": "code",
"execution_count": 60,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[(4, 'cornflowerblue'),\n",
" (3, 'cornflowerblue'),\n",
" (8, 'cornflowerblue'),\n",
" (5, 'pink'),\n",
" (8, 'pink'),\n",
" (1, 'pink'),\n",
" (3, 'pink'),\n",
" (2, 'pink')]"
]
},
"execution_count": 60,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"VIS_COLOUR_MAP = {'md':'cornflowerblue','code':'pink'}\n",
"\n",
"def cell_attrib(cell, colour='cell_type', size='n_screen_lines'):\n",
" _colour = VIS_COLOUR_MAP[ cell[colour] ]\n",
" return (cell[size], _colour)\n",
"\n",
"report.apply(cell_attrib, axis=1).to_list()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's create a function to visualise a notebook based on its list of cell size and colour tuples; we'll also allow it to habdle multiple lists:"
]
},
{
"cell_type": "code",
"execution_count": 92,
"metadata": {},
"outputs": [],
"source": [
"def nb_vis(cell_map, w=20, gap_boost=1, **kwargs):\n",
" \"\"\"Visualise notebook gross cell structure.\"\"\"\n",
" \n",
" def get_gap(cell_map):\n",
" \"\"\"Automatically set the gap value based on overall length\"\"\"\n",
" \n",
" def get_overall_length(cell_map):\n",
" \"\"\"Get overall length of a notebook.\"\"\"\n",
" overall_len = 0\n",
" gap = 0\n",
" for i ,(l,t) in enumerate(cell_map):\n",
" #i is number of cells if that's useful too?\n",
" overall_len = overall_len + l\n",
" return overall_len\n",
"\n",
" max_overall_len = 0\n",
" \n",
" if isinstance(cell_map,dict):\n",
" for k in cell_map:\n",
" _overall_len = get_overall_length(cell_map[k])\n",
" max_overall_len = _overall_len if _overall_len > max_overall_len else max_overall_len\n",
" else:\n",
" max_overall_len = get_overall_length(cell_map)\n",
"\n",
" #Set the gap at 0.5% of the overall length\n",
" return math.ceil(max_overall_len * 0.01)\n",
" \n",
" \n",
" def plotter(cell_map, x, y, label='', header_gap = 0.2,\n",
" linewidth = 5,\n",
" orientation ='v', gap_colour = 'lightgrey'):\n",
" \"\"\"Plot visualisation of gross cell structure for a single notebook.\"\"\"\n",
" \n",
" if orientation =='v':\n",
" plt.text(x, y, label)\n",
" y = y + header_gap\n",
" else:\n",
" plt.text(y, x, label)\n",
" x = x + header_gap\n",
" \n",
" for _cell_map in cell_map:\n",
" _y = y + gap if gap_colour else y\n",
" __y = _y + _cell_map[0] + 1 #Make tiny cells slightly bigger\n",
" \n",
" if orientation =='v':\n",
" X = _X = __X = x\n",
" Y = y\n",
" _Y =_y\n",
" __Y = __y\n",
" else:\n",
" X = y\n",
" _X = _y\n",
" __X = __y\n",
" Y = _Y = __Y = x\n",
" \n",
" #Add a coloured bar between cells\n",
" if y > 0:\n",
" if gap_colour:\n",
" plt.plot([X,_X],[Y,_Y], gap_colour, linewidth=linewidth)\n",
"\n",
" \n",
" plt.plot([_X,__X], [_Y,__Y], _cell_map[1], linewidth=linewidth)\n",
"\n",
" y = __y\n",
"\n",
" x=0\n",
" y=0\n",
" \n",
" if isinstance(cell_map,list):\n",
" gap = get_gap(cell_map) * gap_boost\n",
" fig, ax = plt.subplots(figsize=(w, 1))\n",
" plotter(cell_map, x, y, **kwargs)\n",
" elif isinstance(cell_map,dict):\n",
" gap = get_gap(cell_map) * gap_boost\n",
" fig, ax = plt.subplots(figsize=(w,len(cell_map)))\n",
" for k in cell_map:\n",
" plotter(cell_map[k], x, y, k, **kwargs)\n",
" x = x + 1\n",
"\n",
" ax.axis('off')\n",
" plt.gca().invert_yaxis()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now easily create a simple visualisation of the gross cell structure of the notebook:"
]
},
{
"cell_type": "code",
"execution_count": 93,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAABGoAAABECAYAAADZXtNTAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAACRUlEQVR4nO3cIU7FQBRAUUoQeBxrQH4Nq0GzFgwLYg2sAffRuEFgECgyzVzScxbw8pppzc2k2xjjAgAAAID1LlcvAAAAAMA3oQYAAAAgQqgBAAAAiBBqAAAAACKEGgAAAIAIoQYAAAAgQqgBAAAAiBBqAAAAACKEGgAAAIAIoQYAAAAgQqgBAAAAiBBqAAAAACKEGgAAAIAIoQYAAAAgQqgBAAAAiBBqAAAAACKEGgAAAIAIoQYAAAAgQqgBAAAAiBBqAAAAACKEGgAAAIAIoQYAAAAgQqgBAAAAiBBqAAAAACKEGgAAAIAIoQYAAAAgQqgBAAAAiBBqAAAAACKEGgAAAIAIoQYAAAAgQqgBAAAAiLhavcDeHl8+xuod+N3T/fv0mc+vt9NnHpXzgb/Z49u5O39On0nX28319JneIY7gqN/OUZ/7v5h9Ps7mh4fTtnqFvbhRAwAAABAh1AAAAABECDUAAAAAEUINAAAAQMQ2hn/tAgAAABS4UQMAAAAQIdQAAAAARAg1AAAAABFCDQAAAECEUAMAAAAQIdQAAAAARAg1AAAAABFCDQAAAECEUAMAAAAQIdQAAAAARAg1AAAAABFCDQAAAECEUAMAAAAQIdQAAAAARAg1AAAAABFCDQAAAECEUAMAAAAQIdQAAAAARAg1AAAAABFCDQAAAECEUAMAAAAQIdQAAAAARAg1AAAAABFCDQAAAECEUAMAAAAQIdQAAAAARAg1AAAAABFCDQAAAECEUAMAAAAQIdQAAAAARHwBwKcfg5S0YWYAAAAASUVORK5CYII=\n",
"text/plain": [
"<Figure size 1440x72 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"cell_mapping = report.apply(cell_attrib, axis=1).to_list()\n",
"nb_vis(cell_mapping, orientation='h')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can alo visualisation multiple notebooks, labelling each with the notebook name and plotted against the same length axis so that we can compare notebook sizes and structures directly."
]
},
{
"cell_type": "code",
"execution_count": 94,
"metadata": {
"scrolled": false
},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 1440x288 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"nb_vis({'a':cell_mapping, 'b':cell_mapping[:3],\n",
" 'c':cell_mapping+cell_mapping, 'd':cell_mapping,}, orientation='h')"
]
},
{
"cell_type": "code",
"execution_count": 88,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 1440x432 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"def cell_attribs(cells, colour='cell_type', size='n_screen_lines'):\n",
" return cells.apply(cell_attrib, axis=1, args=(colour,size)).to_list()\n",
"\n",
"zz = ddf.groupby(['filename'])[['cell_type', 'n_screen_lines']].apply(cell_attribs)\n",
"nb_vis(zz.to_dict(), orientation='h', gap_boost=1)\n",
"#[['n_total_code_lines','n_words','reading_time_mins', 'reading_time_s' ]].sum()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can also see how they look based on reading time."
]
},
{
"cell_type": "code",
"execution_count": 79,
"metadata": {},
"outputs": [
{
"data": {
"image/png": "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\n",
"text/plain": [
"<Figure size 1440x432 with 1 Axes>"
]
},
"metadata": {
"needs_background": "light"
},
"output_type": "display_data"
}
],
"source": [
"zz = ddf.groupby(['filename'])[['cell_type', 'reading_time_s']].apply(cell_attribs,'cell_type','reading_time_s')\n",
"nb_vis(zz.to_dict(), orientation='h', gap_boost=2)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Visualing Intra-Cell Structure\n",
"\n",
"For example, paragraphs and code blocks in markdown cells; comment lines, empty lines, code lines, magic lines / blocks, shell command lines in code cells.\n",
"\n",
"Supporting the level of detail may be be tricky. A multi-column format is probably best showing eg an approximate \"screen's worth\" of content in a column then the next \"scroll\" down displayed in the next column along."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"\n",
"# BELOW HERE - NOTES AND TO DO"
]
},
{
"cell_type": "code",
"execution_count": 66,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"100.0"
]
},
"execution_count": 66,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#Maintainability index\n",
"from radon.metrics import mi_visit\n",
"\n",
"#If True, then count multiline strings as comment lines as well.\n",
"#This is not always safe because Python multiline strings are not always docstrings.\n",
"\n",
"multi = True\n",
"mi_visit(c,multi)"
]
},
{
"cell_type": "code",
"execution_count": 67,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'\\nthe Halstead Volume\\nthe Cyclomatic Complexity\\nthe number of LLOC (Logical Lines of Code)\\nthe percent of lines of comment\\n'"
]
},
"execution_count": 67,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from radon.metrics import mi_parameters\n",
"mi_parameters(c, multi)\n",
"\n",
"\"\"\"\n",
"the Halstead Volume\n",
"the Cyclomatic Complexity\n",
"the number of LLOC (Logical Lines of Code)\n",
"the percent of lines of comment\n",
"\"\"\""
]
},
{
"cell_type": "code",
"execution_count": 68,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[]"
]
},
"execution_count": 68,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from radon.complexity import cc_visit\n",
"\n",
"#Doesn't like %% or % magic\n",
"cc_visit(c)"
]
},
{
"cell_type": "code",
"execution_count": 69,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Halstead(total=HalsteadReport(h1=0, h2=0, N1=0, N2=0, vocabulary=0, length=0, calculated_length=0, volume=0, difficulty=0, effort=0, time=0.0, bugs=0.0), functions=[])"
]
},
"execution_count": 69,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from radon.metrics import h_visit\n",
"h_visit(c)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Checking Notebook Metrics Evolution Over Time\n",
"\n",
"The `wily` package uses `radon` to produce code quality reports across a git repository history and generate charts showin the evolution of metrics over the lifetime of a repository. This suggests various corollaries:\n",
"\n",
"- could we generate `wily` style measures over the recent history of a notebook code cell?\n",
"- could we generate `wily` style temporal measures over all the reports (markdown text, as well as code) generated from a notebook across several commits of it to a git repository."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Other Cell Analysis\n",
"\n",
"As a placeholder, should we also at least report on a count of cells that are note code or markdown cells?\n",
"\n",
"Also a count of empty cells?\n",
"\n",
"Is this moving towards some sort of notebook linter?"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.5"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment